Skip to content

Commit 629087b

Browse files
committed
Fix invalid redirect url scheme behind reverse proxy
This commit refactors the OIDC tests to use a more robust fake OIDC provider and improves the logout URL generation to correctly handle the scheme. Fixes #1174
1 parent 8c394ff commit 629087b

File tree

2 files changed

+72
-13
lines changed

2 files changed

+72
-13
lines changed

src/webserver/oidc.rs

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,6 @@ fn get_app_host(config: &AppConfig) -> String {
155155
host
156156
}
157157

158-
fn build_absolute_uri(app_host: &str, relative_path: &str, scheme: &str) -> anyhow::Result<String> {
159-
let mut base_url = Url::parse(&format!("{scheme}://{app_host}"))
160-
.with_context(|| format!("Failed to parse app_host: {app_host}"))?;
161-
base_url.set_path("");
162-
let absolute_url = base_url
163-
.join(relative_path)
164-
.with_context(|| format!("Failed to join path {relative_path}"))?;
165-
Ok(absolute_url.to_string())
166-
}
167-
168158
pub struct ClientWithTime {
169159
client: OidcClient,
170160
end_session_endpoint: Option<EndSessionUrl>,
@@ -246,6 +236,29 @@ impl OidcState {
246236
.map_err(|e| anyhow::anyhow!("Could not verify the ID token: {e}"))?;
247237
Ok(claims)
248238
}
239+
240+
/// Builds an absolute redirect URI by joining the relative redirect URI with the client's redirect URL
241+
pub async fn build_absolute_redirect_uri(
242+
&self,
243+
relative_redirect_uri: &str,
244+
) -> anyhow::Result<String> {
245+
let client_guard = self.get_client().await;
246+
let client_redirect_url = client_guard
247+
.redirect_uri()
248+
.ok_or_else(|| anyhow!("OIDC client has no redirect URL configured"))?;
249+
let absolute_redirect_uri = client_redirect_url
250+
.url()
251+
.join(relative_redirect_uri)
252+
.with_context(|| {
253+
format!(
254+
"Failed to join redirect URI {} with client redirect URL {}",
255+
relative_redirect_uri,
256+
client_redirect_url.url()
257+
)
258+
})?
259+
.to_string();
260+
Ok(absolute_redirect_uri)
261+
}
249262
}
250263

251264
pub async fn initialize_oidc_state(
@@ -494,11 +507,12 @@ async fn process_oidc_logout(
494507
.ok()
495508
.flatten();
496509

497-
let scheme = request.connection_info().scheme().to_string();
498510
let mut response =
499511
if let Some(end_session_endpoint) = oidc_state.get_end_session_endpoint().await {
500-
let absolute_redirect_uri =
501-
build_absolute_uri(&oidc_state.config.app_host, &params.redirect_uri, &scheme)?;
512+
let absolute_redirect_uri = oidc_state
513+
.build_absolute_redirect_uri(&params.redirect_uri)
514+
.await?;
515+
502516
let post_logout_redirect_uri =
503517
PostLogoutRedirectUrl::new(absolute_redirect_uri.clone()).with_context(|| {
504518
format!("Invalid post_logout_redirect_uri: {absolute_redirect_uri}")

tests/oidc/mod.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ struct DiscoveryResponse {
6464
response_types_supported: Vec<String>,
6565
subject_types_supported: Vec<String>,
6666
id_token_signing_alg_values_supported: Vec<String>,
67+
end_session_endpoint: String,
6768
}
6869

6970
#[derive(Serialize)]
@@ -89,6 +90,7 @@ async fn discovery_endpoint(state: Data<SharedProviderState>) -> impl Responder
8990
response_types_supported: vec!["code".to_string()],
9091
subject_types_supported: vec!["public".to_string()],
9192
id_token_signing_alg_values_supported: vec!["HS256".to_string()],
93+
end_session_endpoint: format!("{}/logout", state.issuer_url),
9294
};
9395
HttpResponse::Ok()
9496
.insert_header((header::CONTENT_TYPE, "application/json"))
@@ -435,3 +437,46 @@ async fn test_oidc_expired_token_is_rejected() {
435437
})
436438
.await;
437439
}
440+
441+
#[actix_web::test]
442+
async fn test_oidc_logout_uses_correct_scheme() {
443+
use sqlpage::{
444+
app_config::{test_database_url, AppConfig},
445+
webserver::oidc::create_logout_url,
446+
AppState,
447+
};
448+
449+
crate::common::init_log();
450+
let provider = FakeOidcProvider::new().await;
451+
452+
let db_url = test_database_url();
453+
let config_json = format!(
454+
r#"{{
455+
"database_url": "{db_url}",
456+
"oidc_issuer_url": "{}",
457+
"oidc_client_id": "{}",
458+
"oidc_client_secret": "{}",
459+
"https_domain": "example.com"
460+
}}"#,
461+
provider.issuer_url, provider.client_id, provider.client_secret
462+
);
463+
464+
let config: AppConfig = serde_json::from_str(&config_json).unwrap();
465+
let app_state = AppState::init(&config).await.unwrap();
466+
let app = test::init_service(create_app(Data::new(app_state))).await;
467+
468+
let logout_path = create_logout_url("/logged_out", "", &provider.client_secret);
469+
// make sure the logout path includes the configured domain
470+
assert!(logout_path.starts_with("/sqlpage/oidc_logout"));
471+
472+
let req = test::TestRequest::get().uri(&logout_path).to_request();
473+
let resp = test::call_service(&app, req).await;
474+
475+
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
476+
let location = resp.headers().get("location").unwrap().to_str().unwrap();
477+
let location_url = Url::parse(location).unwrap();
478+
assert_eq!(location_url.path(), "/logout");
479+
let params: HashMap<String, String> = location_url.query_pairs().into_owned().collect();
480+
let post_logout = params.get("post_logout_redirect_uri").unwrap();
481+
assert_eq!(post_logout, "https://example.com/logged_out");
482+
}

0 commit comments

Comments
 (0)