Skip to content

Commit 6cc9b1d

Browse files
authored
Fix: Prevent infinite OIDC redirects (#1135)
* Fix: Prevent infinite OIDC redirects This commit adds a mechanism to prevent infinite redirects in the OIDC callback flow. It does this by: - Tracking the number of redirects using a cookie. - Setting a maximum number of redirects (3). - Returning an error if the maximum is exceeded. * Merge branch 'main' into prevent-oidc-infinite-redirects * simplify OIDC infinite redirect prevention logic This update introduces a new function, `handle_oidc_callback_error`, to streamline error handling during OIDC callback processing. It enhances the management of redirect counts and separates the logic for handling maximum redirect limits into `handle_max_redirect_count_reached`. Additionally, the `build_auth_provider_redirect_response` function is updated to accept the redirect count, ensuring accurate tracking of redirects. This refactor aims to prevent infinite redirect loops and improve code clarity.
1 parent a40b70d commit 6cc9b1d

File tree

2 files changed

+74
-8
lines changed

2 files changed

+74
-8
lines changed

src/webserver/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ fn error_to_html_string(app_state: &AppState, err: &anyhow::Error) -> anyhow::Re
5252
Ok(out.into_string()?)
5353
}
5454

55-
fn anyhow_err_to_actix_resp(e: &anyhow::Error, state: &AppState) -> HttpResponse {
55+
pub(super) fn anyhow_err_to_actix_resp(e: &anyhow::Error, state: &AppState) -> HttpResponse {
5656
let mut resp = HttpResponseBuilder::new(StatusCode::INTERNAL_SERVER_ERROR);
5757
resp.insert_header((header::CONTENT_TYPE, header::ContentType::plaintext()));
5858

src/webserver/oidc.rs

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use openidconnect::{
3636
use serde::{Deserialize, Serialize};
3737
use tokio::sync::{RwLock, RwLockReadGuard};
3838

39+
use super::error::anyhow_err_to_actix_resp;
3940
use super::http_client::make_http_client;
4041

4142
type LocalBoxFuture<T> = Pin<Box<dyn Future<Output = T> + 'static>>;
@@ -47,6 +48,8 @@ const SQLPAGE_NONCE_COOKIE_NAME: &str = "sqlpage_oidc_nonce";
4748
const SQLPAGE_TMP_LOGIN_STATE_COOKIE_PREFIX: &str = "sqlpage_oidc_state_";
4849
const OIDC_CLIENT_MAX_REFRESH_INTERVAL: Duration = Duration::from_secs(60 * 60);
4950
const OIDC_CLIENT_MIN_REFRESH_INTERVAL: Duration = Duration::from_secs(5);
51+
const SQLPAGE_OIDC_REDIRECT_COUNT_COOKIE: &str = "sqlpage_oidc_redirect_count";
52+
const MAX_OIDC_REDIRECTS: u8 = 3;
5053
const AUTH_COOKIE_EXPIRATION: awc::cookie::time::Duration =
5154
actix_web::cookie::time::Duration::days(7);
5255
const LOGIN_FLOW_STATE_COOKIE_EXPIRATION: awc::cookie::time::Duration =
@@ -400,20 +403,50 @@ async fn handle_unauthenticated_request(
400403
log::debug!("Redirecting to OIDC provider");
401404

402405
let initial_url = request.uri().to_string();
403-
let response = build_auth_provider_redirect_response(oidc_state, &initial_url).await;
406+
let redirect_count = get_redirect_count(&request);
407+
let response =
408+
build_auth_provider_redirect_response(oidc_state, &initial_url, redirect_count).await;
404409
MiddlewareResponse::Respond(request.into_response(response))
405410
}
406411

407412
async fn handle_oidc_callback(oidc_state: &OidcState, request: ServiceRequest) -> ServiceResponse {
408413
match process_oidc_callback(oidc_state, &request).await {
409-
Ok(response) => request.into_response(response),
410-
Err(e) => {
411-
log::error!("Failed to process OIDC callback. Refreshing oidc provider metadata, then redirecting to home page: {e:#}");
412-
oidc_state.refresh_on_error(&request).await;
413-
let resp = build_auth_provider_redirect_response(oidc_state, "/").await;
414-
request.into_response(resp)
414+
Ok(mut response) => {
415+
clear_redirect_count_cookie(&mut response);
416+
request.into_response(response)
415417
}
418+
Err(e) => handle_oidc_callback_error(oidc_state, request, e).await,
419+
}
420+
}
421+
422+
async fn handle_oidc_callback_error(
423+
oidc_state: &OidcState,
424+
request: ServiceRequest,
425+
e: anyhow::Error,
426+
) -> ServiceResponse {
427+
let redirect_count = get_redirect_count(&request);
428+
if redirect_count >= MAX_OIDC_REDIRECTS {
429+
return handle_max_redirect_count_reached(request, &e, redirect_count);
416430
}
431+
log::error!(
432+
"Failed to process OIDC callback (attempt {redirect_count}). Refreshing oidc provider metadata, then redirecting to home page: {e:#}"
433+
);
434+
oidc_state.refresh_on_error(&request).await;
435+
let resp = build_auth_provider_redirect_response(oidc_state, "/", redirect_count).await;
436+
request.into_response(resp)
437+
}
438+
439+
fn handle_max_redirect_count_reached(
440+
request: ServiceRequest,
441+
e: &anyhow::Error,
442+
redirect_count: u8,
443+
) -> ServiceResponse {
444+
log::error!(
445+
"Failed to process OIDC callback after {redirect_count} attempts. \
446+
Stopping to avoid infinite redirections: {e:#}"
447+
);
448+
let resp = build_oidc_error_response(&request, e);
449+
request.into_response(resp)
417450
}
418451

419452
async fn handle_oidc_logout(oidc_state: &OidcState, request: ServiceRequest) -> ServiceResponse {
@@ -668,12 +701,23 @@ fn set_auth_cookie(response: &mut HttpResponse, id_token: &OidcToken) {
668701
async fn build_auth_provider_redirect_response(
669702
oidc_state: &OidcState,
670703
initial_url: &str,
704+
redirect_count: u8,
671705
) -> HttpResponse {
672706
let AuthUrl { url, params } = build_auth_url(oidc_state).await;
673707
let tmp_login_flow_state_cookie = create_tmp_login_flow_state_cookie(&params, initial_url);
708+
let redirect_count_cookie = Cookie::build(
709+
SQLPAGE_OIDC_REDIRECT_COUNT_COOKIE,
710+
(redirect_count + 1).to_string(),
711+
)
712+
.path("/")
713+
.http_only(true)
714+
.same_site(actix_web::cookie::SameSite::Lax)
715+
.max_age(LOGIN_FLOW_STATE_COOKIE_EXPIRATION)
716+
.finish();
674717
HttpResponse::SeeOther()
675718
.append_header((header::LOCATION, url.to_string()))
676719
.cookie(tmp_login_flow_state_cookie)
720+
.cookie(redirect_count_cookie)
677721
.body("Redirecting...")
678722
}
679723

@@ -683,6 +727,28 @@ fn build_redirect_response(target_url: String) -> HttpResponse {
683727
.body("Redirecting...")
684728
}
685729

730+
fn get_redirect_count(request: &ServiceRequest) -> u8 {
731+
request
732+
.cookie(SQLPAGE_OIDC_REDIRECT_COUNT_COOKIE)
733+
.and_then(|c| c.value().parse().ok())
734+
.unwrap_or(0)
735+
}
736+
737+
fn clear_redirect_count_cookie(response: &mut HttpResponse) {
738+
let cookie = Cookie::build(SQLPAGE_OIDC_REDIRECT_COUNT_COOKIE, "")
739+
.path("/")
740+
.finish()
741+
.into_owned();
742+
response.add_removal_cookie(&cookie).ok();
743+
}
744+
745+
fn build_oidc_error_response(request: &ServiceRequest, e: &anyhow::Error) -> HttpResponse {
746+
request.app_data::<web::Data<AppState>>().map_or_else(
747+
|| HttpResponse::InternalServerError().body(format!("Authentication error: {e}")),
748+
|state| anyhow_err_to_actix_resp(e, state),
749+
)
750+
}
751+
686752
/// Returns the claims from the ID token in the `SQLPage` auth cookie.
687753
async fn get_authenticated_user_info(
688754
oidc_state: &OidcState,

0 commit comments

Comments
 (0)