Skip to content

Commit 00c6b41

Browse files
t-aleksanderteonj-chmielewskiAdam CiarcińskiMaciek
authored
merge dev -> main (#100)
* skip manual config step * better ux * eslint * cleanup * update protobufs (#88) * feat: config polling (#86) * CI: fix re-creating manifests * chore: log version with git commit hash on startup (#89) * update protobufs (#90) * Rework instance config fetching (#91) * instance config fetching rework * update protobufs * add teonite link (#92) * add link * noreferrer * add defguard link * Basic nix flake without rust * Flake update * enable ARMv7 build (#93) Co-authored-by: Maciej Wójcik <maciek@wjck.pl> * Make a pre-release and release docker build workflow (#94) * split builds * fix vergen * add flavor to build-docker workflow * bump version to 1.0.0 (#95) * OpenID via Proxy (#97) * Handle auth info * Use AuthInfoRequest * Handle AuthCallback * Use Url crate for URL option * add frontend * translations, id_token -> code * more translations, cleanup * cleanup * move to enterprise folder --------- Co-authored-by: Aleksander <170264518+t-aleksander@users.noreply.github.com> * Change nonce and csrf cookie names (#99) * change cookies name * bump version * fix cargo lock --------- Co-authored-by: Robert Olejnik <r@nxt.cx> Co-authored-by: Jacek Chmielewski <jchmielewski@teonite.com> Co-authored-by: Adam Ciarciński <aciarcinski@teonite.com> Co-authored-by: Maciek <mwojcik@teonite.com> Co-authored-by: Maciej Wójcik <maciek@wjck.pl>
1 parent 44d968f commit 00c6b41

32 files changed

Lines changed: 1773 additions & 454 deletions

File tree

Cargo.lock

Lines changed: 568 additions & 358 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "defguard-proxy"
3-
version = "1.0.0"
3+
version = "1.1.0"
44
edition = "2021"
55
license = "Apache-2.0"
66
homepage = "https://github.com/DefGuard/proxy"
@@ -38,7 +38,7 @@ anyhow = "1.0"
3838
clap = { version = "4.5", features = ["derive", "env", "cargo"] }
3939
# other utils
4040
dotenvy = "0.15"
41-
url = "2.5"
41+
url = { version = "2.5", features = ["serde"] }
4242
tower_governor = "0.4"
4343
# UI embedding
4444
rust-embed = { version = "8.5", features = ["include-exclude"] }

build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
1111
config.protoc_arg("--experimental_allow_proto3_optional");
1212
// Make all messages serde-serializable
1313
config.type_attribute(".", "#[derive(serde::Serialize,serde::Deserialize)]");
14-
tonic_build::configure().compile_with_config(
14+
tonic_build::configure().compile_protos_with_config(
1515
config,
1616
&["proto/core/proxy.proto"],
1717
&["proto/core"],

proto

src/config.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use std::{fs, io::Error as IoError};
1+
use std::{fs::read_to_string, path::PathBuf};
22

33
use clap::Parser;
44
use log::LevelFilter;
55
use serde::Deserialize;
6+
use url::Url;
67

78
#[derive(Parser, Debug, Deserialize)]
89
#[command(version)]
@@ -35,16 +36,24 @@ pub struct Config {
3536
#[arg(long, env = "DEFGUARD_PROXY_RATELIMIT_BURST", default_value_t = 0)]
3637
pub rate_limit_burst: u32,
3738

39+
#[arg(
40+
long,
41+
env = "DEFGUARD_PROXY_URL",
42+
value_parser = Url::parse,
43+
default_value = "http://localhost:8080"
44+
)]
45+
pub url: Url,
46+
3847
/// Configuration file path
3948
#[arg(long = "config", short)]
4049
#[serde(skip)]
41-
config_path: Option<std::path::PathBuf>,
50+
config_path: Option<PathBuf>,
4251
}
4352

4453
#[derive(thiserror::Error, Debug)]
4554
pub enum ConfigError {
4655
#[error("Failed to read config file")]
47-
IoError(#[from] IoError),
56+
IoError(#[from] std::io::Error),
4857
#[error("Failed to parse config file")]
4958
ParseError(#[from] toml::de::Error),
5059
}
@@ -55,11 +64,11 @@ pub fn get_config() -> Result<Config, ConfigError> {
5564

5665
// load config from file if one was specified
5766
if let Some(config_path) = cli_config.config_path {
58-
info!("Reading configuration from config file: {config_path:?}");
59-
let config_toml = fs::read_to_string(config_path)?;
67+
info!("Reading configuration from file: {config_path:?}");
68+
let config_toml = read_to_string(config_path)?;
6069
let file_config: Config = toml::from_str(&config_toml)?;
61-
return Ok(file_config);
70+
Ok(file_config)
71+
} else {
72+
Ok(cli_config)
6273
}
63-
64-
Ok(cli_config)
6574
}

src/enterprise/handlers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod openid_login;
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
use axum::{
2+
extract::State,
3+
routing::{get, post},
4+
Json, Router,
5+
};
6+
use axum_extra::extract::{
7+
cookie::{Cookie, SameSite},
8+
PrivateCookieJar,
9+
};
10+
use serde::{Deserialize, Serialize};
11+
use time::Duration;
12+
13+
use crate::{
14+
error::ApiError,
15+
handlers::get_core_response,
16+
http::AppState,
17+
proto::{
18+
core_request, core_response, AuthCallbackRequest, AuthCallbackResponse, AuthInfoRequest,
19+
},
20+
};
21+
22+
const COOKIE_MAX_AGE: Duration = Duration::days(1);
23+
static CSRF_COOKIE_NAME: &str = "csrf_proxy";
24+
static NONCE_COOKIE_NAME: &str = "nonce_proxy";
25+
26+
pub(crate) fn router() -> Router<AppState> {
27+
Router::new()
28+
.route("/auth_info", get(auth_info))
29+
.route("/callback", post(auth_callback))
30+
}
31+
32+
#[derive(Serialize)]
33+
struct AuthInfo {
34+
url: String,
35+
button_display_name: Option<String>,
36+
}
37+
38+
impl AuthInfo {
39+
#[must_use]
40+
fn new(url: String, button_display_name: Option<String>) -> Self {
41+
Self {
42+
url,
43+
button_display_name,
44+
}
45+
}
46+
}
47+
48+
/// Request external OAuth2/OpenID provider details from Defguard Core.
49+
#[instrument(level = "debug", skip(state))]
50+
async fn auth_info(
51+
State(state): State<AppState>,
52+
private_cookies: PrivateCookieJar,
53+
) -> Result<(PrivateCookieJar, Json<AuthInfo>), ApiError> {
54+
debug!("Getting auth info for OAuth2/OpenID login");
55+
56+
let request = AuthInfoRequest {
57+
redirect_url: state.callback_url().to_string(),
58+
};
59+
60+
let rx = state
61+
.grpc_server
62+
.send(Some(core_request::Payload::AuthInfo(request)), None)?;
63+
let payload = get_core_response(rx).await?;
64+
if let core_response::Payload::AuthInfo(response) = payload {
65+
debug!("Received auth info {response:?}");
66+
67+
let nonce_cookie = Cookie::build((NONCE_COOKIE_NAME, response.nonce))
68+
// .domain(cookie_domain)
69+
.path("/api/v1/openid/callback")
70+
.http_only(true)
71+
.same_site(SameSite::Strict)
72+
.secure(true)
73+
.max_age(COOKIE_MAX_AGE)
74+
.build();
75+
let csrf_cookie = Cookie::build((CSRF_COOKIE_NAME, response.csrf_token))
76+
// .domain(cookie_domain)
77+
.path("/api/v1/openid/callback")
78+
.http_only(true)
79+
.same_site(SameSite::Strict)
80+
.secure(true)
81+
.max_age(COOKIE_MAX_AGE)
82+
.build();
83+
let private_cookies = private_cookies.add(nonce_cookie).add(csrf_cookie);
84+
85+
let auth_info = AuthInfo::new(response.url, response.button_display_name);
86+
Ok((private_cookies, Json(auth_info)))
87+
} else {
88+
error!("Received invalid gRPC response type: {payload:#?}");
89+
Err(ApiError::InvalidResponseType)
90+
}
91+
}
92+
93+
#[derive(Debug, Deserialize)]
94+
pub struct AuthenticationResponse {
95+
code: String,
96+
state: String,
97+
}
98+
99+
#[derive(Serialize)]
100+
struct CallbackResponseData {
101+
url: String,
102+
token: String,
103+
}
104+
105+
#[instrument(level = "debug", skip(state))]
106+
async fn auth_callback(
107+
State(state): State<AppState>,
108+
mut private_cookies: PrivateCookieJar,
109+
Json(payload): Json<AuthenticationResponse>,
110+
) -> Result<(PrivateCookieJar, Json<CallbackResponseData>), ApiError> {
111+
let nonce = private_cookies
112+
.get(NONCE_COOKIE_NAME)
113+
.ok_or(ApiError::Unauthorized("Nonce cookie not found".into()))?
114+
.value_trimmed()
115+
.to_string();
116+
let csrf = private_cookies
117+
.get(CSRF_COOKIE_NAME)
118+
.ok_or(ApiError::Unauthorized("CSRF cookie not found".into()))?
119+
.value_trimmed()
120+
.to_string();
121+
122+
if payload.state != csrf {
123+
return Err(ApiError::Unauthorized("CSRF token mismatch".into()));
124+
}
125+
126+
private_cookies = private_cookies
127+
.remove(Cookie::from(NONCE_COOKIE_NAME))
128+
.remove(Cookie::from(CSRF_COOKIE_NAME));
129+
130+
let request = AuthCallbackRequest {
131+
code: payload.code,
132+
nonce,
133+
callback_url: state.callback_url().to_string(),
134+
};
135+
136+
let rx = state
137+
.grpc_server
138+
.send(Some(core_request::Payload::AuthCallback(request)), None)?;
139+
let payload = get_core_response(rx).await?;
140+
if let core_response::Payload::AuthCallback(AuthCallbackResponse { url, token }) = payload {
141+
debug!("Received auth callback response {url:?} {token:?}");
142+
Ok((private_cookies, Json(CallbackResponseData { url, token })))
143+
} else {
144+
error!("Received invalid gRPC response type during handling the OpenID authentication callback: {payload:#?}");
145+
Err(ApiError::InvalidResponseType)
146+
}
147+
}

src/enterprise/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod handlers;

src/grpc.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub(crate) struct ProxyServer {
3030
impl ProxyServer {
3131
#[must_use]
3232
/// Create new `ProxyServer`.
33-
pub fn new() -> Self {
33+
pub(crate) fn new() -> Self {
3434
Self {
3535
current_id: Arc::new(AtomicU64::new(1)),
3636
clients: Arc::new(Mutex::new(HashMap::new())),
@@ -42,7 +42,7 @@ impl ProxyServer {
4242
/// Sends message to the other side of RPC, with given `payload` and optional `device_info`.
4343
/// Returns `tokio::sync::oneshot::Reveicer` to let the caller await reply.
4444
#[instrument(name = "send_grpc_message", level = "debug", skip(self))]
45-
pub fn send(
45+
pub(crate) fn send(
4646
&self,
4747
payload: Option<core_request::Payload>,
4848
device_info: Option<DeviceInfo>,
@@ -64,9 +64,11 @@ impl ProxyServer {
6464
self.connected.store(true, Ordering::Relaxed);
6565
Ok(rx)
6666
} else {
67-
error!("Defguard core is disconnected");
67+
error!("Defguard Core is not connected");
6868
self.connected.store(false, Ordering::Relaxed);
69-
Err(ApiError::Unexpected("Defguard core is disconnected".into()))
69+
Err(ApiError::Unexpected(
70+
"Defguard Core is not connected".into(),
71+
))
7072
}
7173
}
7274
}
@@ -96,7 +98,7 @@ impl proxy_server::Proxy for ProxyServer {
9698
error!("Failed to determine client address for request: {request:?}");
9799
return Err(Status::internal("Failed to determine client address"));
98100
};
99-
info!("Defguard core RPC client connected from: {address}");
101+
info!("Defguard Core gRPC client connected from: {address}");
100102

101103
let (tx, rx) = mpsc::unbounded_channel();
102104
self.clients.lock().unwrap().insert(address, tx);

src/handlers/enrollment.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::{
1212
},
1313
};
1414

15-
pub fn router() -> Router<AppState> {
15+
pub(crate) fn router() -> Router<AppState> {
1616
Router::new()
1717
.route("/start", post(start_enrollment_process))
1818
.route("/activate_user", post(activate_user))
@@ -21,7 +21,7 @@ pub fn router() -> Router<AppState> {
2121
}
2222

2323
#[instrument(level = "debug", skip(state))]
24-
pub async fn start_enrollment_process(
24+
async fn start_enrollment_process(
2525
State(state): State<AppState>,
2626
mut private_cookies: PrivateCookieJar,
2727
Json(req): Json<EnrollmentStartRequest>,
@@ -60,7 +60,7 @@ pub async fn start_enrollment_process(
6060
}
6161

6262
#[instrument(level = "debug", skip(state))]
63-
pub async fn activate_user(
63+
async fn activate_user(
6464
State(state): State<AppState>,
6565
device_info: Option<DeviceInfo>,
6666
mut private_cookies: PrivateCookieJar,
@@ -95,7 +95,7 @@ pub async fn activate_user(
9595
}
9696

9797
#[instrument(level = "debug", skip(state))]
98-
pub async fn create_device(
98+
async fn create_device(
9999
State(state): State<AppState>,
100100
device_info: Option<DeviceInfo>,
101101
private_cookies: PrivateCookieJar,
@@ -123,7 +123,7 @@ pub async fn create_device(
123123
}
124124

125125
#[instrument(level = "debug", skip(state))]
126-
pub async fn get_network_info(
126+
async fn get_network_info(
127127
State(state): State<AppState>,
128128
private_cookies: PrivateCookieJar,
129129
Json(mut req): Json<ExistingDevice>,

0 commit comments

Comments
 (0)