@@ -36,6 +36,7 @@ use openidconnect::{
3636use serde:: { Deserialize , Serialize } ;
3737use tokio:: sync:: { RwLock , RwLockReadGuard } ;
3838
39+ use super :: error:: anyhow_err_to_actix_resp;
3940use super :: http_client:: make_http_client;
4041
4142type LocalBoxFuture < T > = Pin < Box < dyn Future < Output = T > + ' static > > ;
@@ -47,6 +48,8 @@ const SQLPAGE_NONCE_COOKIE_NAME: &str = "sqlpage_oidc_nonce";
4748const SQLPAGE_TMP_LOGIN_STATE_COOKIE_PREFIX : & str = "sqlpage_oidc_state_" ;
4849const OIDC_CLIENT_MAX_REFRESH_INTERVAL : Duration = Duration :: from_secs ( 60 * 60 ) ;
4950const 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 ;
5053const AUTH_COOKIE_EXPIRATION : awc:: cookie:: time:: Duration =
5154 actix_web:: cookie:: time:: Duration :: days ( 7 ) ;
5255const 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
407412async 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
419452async fn handle_oidc_logout ( oidc_state : & OidcState , request : ServiceRequest ) -> ServiceResponse {
@@ -668,12 +701,23 @@ fn set_auth_cookie(response: &mut HttpResponse, id_token: &OidcToken) {
668701async 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.
687753async fn get_authenticated_user_info (
688754 oidc_state : & OidcState ,
0 commit comments