Skip to content

Commit d3a9902

Browse files
committed
fix: oidc redirect
- add redirect checks for logout - add `P_ALLOW_ORIGINS` env var to allow redirection to other origins
1 parent 5c20b8e commit d3a9902

3 files changed

Lines changed: 98 additions & 14 deletions

File tree

src/cli.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,17 @@ pub struct Options {
150150
)]
151151
pub address: String,
152152

153+
// Origins to allow for cors
154+
#[arg(
155+
long,
156+
env = "P_ALLOW_ORIGINS",
157+
required = false,
158+
value_delimiter = ',',
159+
value_parser = validation::url,
160+
help = "Comma separated URLs to allow as origins"
161+
)]
162+
pub allow_origins: Vec<Url>,
163+
153164
// Actix request timeout in seconds
154165
#[arg(
155166
long,
@@ -807,3 +818,59 @@ impl Options {
807818
})
808819
}
809820
}
821+
822+
#[cfg(test)]
823+
mod tests {
824+
use super::{Cli, StorageOptions};
825+
use clap::Parser;
826+
827+
#[test]
828+
fn parses_comma_separated_allowed_origins() {
829+
let cli = Cli::parse_from([
830+
"parseable",
831+
"local-store",
832+
"--allowed-origins",
833+
"https://www.google.com/,https://app-staging.parseable.com/",
834+
]);
835+
836+
let StorageOptions::Local(args) = cli.storage else {
837+
panic!("expected local-store args");
838+
};
839+
840+
assert_eq!(args.options.allow_origins.len(), 2);
841+
assert_eq!(
842+
args.options.allow_origins[0].as_str(),
843+
"https://www.google.com/"
844+
);
845+
assert_eq!(
846+
args.options.allow_origins[1].as_str(),
847+
"https://app-staging.parseable.com/"
848+
);
849+
}
850+
851+
#[test]
852+
fn parses_repeated_allowed_origins() {
853+
let cli = Cli::parse_from([
854+
"parseable",
855+
"local-store",
856+
"--allowed-origins",
857+
"https://www.google.com/",
858+
"--allowed-origins",
859+
"https://app-staging.parseable.com/",
860+
]);
861+
862+
let StorageOptions::Local(args) = cli.storage else {
863+
panic!("expected local-store args");
864+
};
865+
866+
assert_eq!(args.options.allow_origins.len(), 2);
867+
assert_eq!(
868+
args.options.allow_origins[0].as_str(),
869+
"https://www.google.com/"
870+
);
871+
assert_eq!(
872+
args.options.allow_origins[1].as_str(),
873+
"https://app-staging.parseable.com/"
874+
);
875+
}
876+
}

src/handlers/http/oidc.rs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ pub fn set_cookie_cross_site(enabled: bool) {
3636
}
3737
use chrono::{Duration, TimeDelta};
3838
use openid::Bearer;
39-
use regex::Regex;
4039
use serde::Deserialize;
4140
use ulid::Ulid;
4241
use url::Url;
@@ -79,8 +78,8 @@ pub async fn login(
7978
query: web::Query<RedirectAfterLogin>,
8079
) -> Result<HttpResponse, OIDCError> {
8180
let conn = req.connection_info().clone();
82-
let base_url_without_scheme = format!("{}/", conn.host());
83-
if !is_valid_redirect_url(&base_url_without_scheme, query.redirect.as_str()) {
81+
let base_url = format!("{}://{}/", conn.scheme(), conn.host());
82+
if !is_valid_redirect_url(&base_url, query.redirect.as_str()) {
8483
return Err(OIDCError::BadRequest(
8584
"Bad Request, Invalid Redirect URL!".to_string(),
8685
));
@@ -167,11 +166,22 @@ pub async fn login(
167166
}
168167
}
169168

170-
pub async fn logout(req: HttpRequest, query: web::Query<RedirectAfterLogin>) -> HttpResponse {
169+
pub async fn logout(
170+
req: HttpRequest,
171+
query: web::Query<RedirectAfterLogin>,
172+
) -> Result<HttpResponse, OIDCError> {
171173
let oidc_client = OIDC_CLIENT.get();
174+
let conn = req.connection_info().clone();
175+
let base_url = format!("{}://{}/", conn.scheme(), conn.host());
176+
177+
if !is_valid_redirect_url(&base_url, query.redirect.as_str()) {
178+
return Err(OIDCError::BadRequest(
179+
"Bad Request, Invalid Redirect URL!".to_string(),
180+
));
181+
}
172182

173183
let Some(session) = extract_session_key_from_req(&req).ok() else {
174-
return redirect_to_client(query.redirect.as_str(), None);
184+
return Ok(redirect_to_client(query.redirect.as_str(), None));
175185
};
176186
let tenant_id = get_tenant_id_from_key(&session);
177187
let user = Users.remove_session(&session);
@@ -185,9 +195,9 @@ pub async fn logout(req: HttpRequest, query: web::Query<RedirectAfterLogin>) ->
185195
(Some(username), Some(logout_endpoint))
186196
if Users.is_oauth(&username, &tenant_id).unwrap_or_default() =>
187197
{
188-
redirect_to_oidc_logout(logout_endpoint, &query.redirect)
198+
Ok(redirect_to_oidc_logout(logout_endpoint, &query.redirect))
189199
}
190-
_ => redirect_to_client(query.redirect.as_str(), None),
200+
_ => Ok(redirect_to_client(query.redirect.as_str(), None)),
191201
}
192202
}
193203

@@ -643,9 +653,17 @@ impl actix_web::ResponseError for OIDCError {
643653
}
644654
}
645655

646-
fn is_valid_redirect_url(base_url_without_scheme: &str, redirect_url: &str) -> bool {
647-
let http_scheme_match_regex = Regex::new(r"^(https?://)").unwrap();
648-
let redirect_url_without_scheme = http_scheme_match_regex.replace(redirect_url, "");
649-
650-
base_url_without_scheme == redirect_url_without_scheme
656+
fn is_valid_redirect_url(base_url: &str, redirect_url: &str) -> bool {
657+
// check if redirect_url is part of allowed origins
658+
if let allowed_origins = &PARSEABLE.options.allow_origins
659+
&& (allowed_origins
660+
.iter()
661+
.any(|url| url.as_str().eq(redirect_url)))
662+
{
663+
true
664+
}
665+
// if redirect_url is not part of allowed origins, then it must be equal to base url
666+
else {
667+
base_url == redirect_url
668+
}
651669
}

src/option.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,10 @@ pub mod validation {
9393
path::{Path, PathBuf},
9494
};
9595

96+
use super::{Compression, Mode};
9697
use crate::cli::DATASET_FIELD_COUNT_LIMIT;
9798
use path_clean::PathClean;
9899

99-
use super::{Compression, Mode};
100-
101100
pub fn file_path(s: &str) -> Result<PathBuf, String> {
102101
if s.is_empty() {
103102
return Err("empty path".to_owned());

0 commit comments

Comments
 (0)