Skip to content

Commit 33ae7c9

Browse files
committed
fix(oidc): respect site_prefix in OIDC redirect and logout URLs
This change ensures that when `site_prefix` is configured, the OIDC redirect URI and logout URI include this prefix. Previously, `site_prefix` was ignored, causing OIDC callbacks to fail when the application was served under a sub-path. - Added `site_prefix` to `OidcConfig`. - Updated `make_oidc_client` to prepend `site_prefix` to the redirect URI. - Updated `handle_request` to match paths with `site_prefix` included. - Updated `validate_redirect_url` to respect the prefix when verifying redirect targets. - Added a regression test `test_oidc_with_site_prefix`.
1 parent 8c394ff commit 33ae7c9

File tree

2 files changed

+75
-7
lines changed

2 files changed

+75
-7
lines changed

src/webserver/oidc.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ pub struct OidcConfig {
7979
pub app_host: String,
8080
pub scopes: Vec<Scope>,
8181
pub additional_audience_verifier: AudienceVerifier,
82+
pub site_prefix: String,
8283
}
8384

8485
impl TryFrom<&AppConfig> for OidcConfig {
@@ -109,6 +110,7 @@ impl TryFrom<&AppConfig> for OidcConfig {
109110
additional_audience_verifier: AudienceVerifier::new(
110111
config.oidc_additional_trusted_audiences.clone(),
111112
),
113+
site_prefix: config.site_prefix.clone(),
112114
})
113115
}
114116
}
@@ -362,12 +364,15 @@ async fn handle_request(oidc_state: &OidcState, request: ServiceRequest) -> Midd
362364
log::trace!("Started OIDC middleware request handling");
363365
oidc_state.refresh_if_expired(&request).await;
364366

365-
if request.path() == SQLPAGE_REDIRECT_URI {
367+
let redirect_uri = format!("{}{}", oidc_state.config.site_prefix.trim_end_matches('/'), SQLPAGE_REDIRECT_URI);
368+
let logout_uri = format!("{}{}", oidc_state.config.site_prefix.trim_end_matches('/'), SQLPAGE_LOGOUT_URI);
369+
370+
if request.path() == redirect_uri {
366371
let response = handle_oidc_callback(oidc_state, request).await;
367372
return MiddlewareResponse::Respond(response);
368373
}
369374

370-
if request.path() == SQLPAGE_LOGOUT_URI {
375+
if request.path() == logout_uri {
371376
let response = handle_oidc_logout(oidc_state, request).await;
372377
return MiddlewareResponse::Respond(response);
373378
}
@@ -640,7 +645,7 @@ async fn process_oidc_callback(
640645
nonce,
641646
redirect_target,
642647
} = parse_login_flow_state(&tmp_login_flow_state_cookie)?;
643-
let redirect_target = validate_redirect_url(redirect_target.to_string());
648+
let redirect_target = validate_redirect_url(redirect_target.to_string(), &oidc_state.config.site_prefix);
644649

645650
log::info!("Redirecting to {redirect_target} after a successful login");
646651
let mut response = build_redirect_response(redirect_target);
@@ -884,9 +889,10 @@ fn make_oidc_client(
884889
let client_id = openidconnect::ClientId::new(config.client_id.clone());
885890
let client_secret = openidconnect::ClientSecret::new(config.client_secret.clone());
886891

892+
let redirect_path = format!("{}{}", config.site_prefix.trim_end_matches('/'), SQLPAGE_REDIRECT_URI);
887893
let mut redirect_url = RedirectUrl::new(format!(
888894
"https://{}{}",
889-
config.app_host, SQLPAGE_REDIRECT_URI,
895+
config.app_host, redirect_path,
890896
))
891897
.with_context(|| {
892898
format!(
@@ -903,7 +909,7 @@ fn make_oidc_client(
903909
log::debug!("App host seems to be local, changing redirect URL to HTTP");
904910
redirect_url = RedirectUrl::new(format!(
905911
"http://{}{}",
906-
config.app_host, SQLPAGE_REDIRECT_URI,
912+
config.app_host, redirect_path,
907913
))?;
908914
}
909915
log::info!("OIDC redirect URL for {}: {redirect_url}", config.client_id);
@@ -1077,8 +1083,9 @@ impl AudienceVerifier {
10771083
}
10781084

10791085
/// Validate that a redirect URL is safe to use (prevents open redirect attacks)
1080-
fn validate_redirect_url(url: String) -> String {
1081-
if url.starts_with('/') && !url.starts_with("//") && !url.starts_with(SQLPAGE_REDIRECT_URI) {
1086+
fn validate_redirect_url(url: String, site_prefix: &str) -> String {
1087+
let redirect_uri = format!("{}{}", site_prefix.trim_end_matches('/'), SQLPAGE_REDIRECT_URI);
1088+
if url.starts_with('/') && !url.starts_with("//") && !url.starts_with(&redirect_uri) {
10821089
return url;
10831090
}
10841091
log::warn!("Refusing to redirect to {url}");

tests/oidc/mod.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,3 +435,64 @@ async fn test_oidc_expired_token_is_rejected() {
435435
})
436436
.await;
437437
}
438+
439+
async fn setup_oidc_test_with_prefix(
440+
provider_mutator: impl FnOnce(&mut ProviderState),
441+
site_prefix: &str,
442+
) -> (
443+
impl actix_web::dev::Service<
444+
actix_http::Request,
445+
Response = actix_web::dev::ServiceResponse<impl actix_web::body::MessageBody>,
446+
Error = actix_web::Error,
447+
>,
448+
FakeOidcProvider,
449+
) {
450+
use sqlpage::{
451+
app_config::{test_database_url, AppConfig},
452+
AppState,
453+
};
454+
crate::common::init_log();
455+
let provider = FakeOidcProvider::new().await;
456+
provider.with_state_mut(provider_mutator);
457+
458+
let db_url = test_database_url();
459+
let config_json = format!(
460+
r#"{{
461+
"database_url": "{db_url}",
462+
"max_database_pool_connections": 1,
463+
"database_connection_retries": 3,
464+
"database_connection_acquire_timeout_seconds": 15,
465+
"allow_exec": true,
466+
"max_uploaded_file_size": 123456,
467+
"listen_on": "127.0.0.1:0",
468+
"system_root_ca_certificates": false,
469+
"oidc_issuer_url": "{}",
470+
"oidc_client_id": "{}",
471+
"oidc_client_secret": "{}",
472+
"oidc_protected_paths": ["/"],
473+
"host": "localhost:1",
474+
"site_prefix": "{site_prefix}"
475+
}}"#,
476+
provider.issuer_url, provider.client_id, provider.client_secret
477+
);
478+
479+
let config: AppConfig = serde_json::from_str(&config_json).unwrap();
480+
let app_state = AppState::init(&config).await.unwrap();
481+
let app = test::init_service(create_app(Data::new(app_state))).await;
482+
(app, provider)
483+
}
484+
485+
#[actix_web::test]
486+
async fn test_oidc_with_site_prefix() {
487+
let (app, _provider) = setup_oidc_test_with_prefix(|_| {}, "/my-app/").await;
488+
let mut cookies: Vec<Cookie<'static>> = Vec::new();
489+
490+
// Access the app with the prefix
491+
let resp = request_with_cookies!(app, test::TestRequest::get().uri("/my-app/"), cookies);
492+
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
493+
let auth_url = Url::parse(resp.headers().get("location").unwrap().to_str().unwrap()).unwrap();
494+
495+
// Check if the redirect_uri parameter in the auth URL contains the site prefix
496+
let redirect_uri = get_query_param(&auth_url, "redirect_uri");
497+
assert!(redirect_uri.contains("/my-app/sqlpage/oidc_callback"), "Redirect URI should contain site prefix. Got: {}", redirect_uri);
498+
}

0 commit comments

Comments
 (0)