@@ -41,11 +41,13 @@ type LocalBoxFuture<T> = Pin<Box<dyn Future<Output = T> + 'static>>;
4141const SQLPAGE_AUTH_COOKIE_NAME : & str = "sqlpage_auth" ;
4242const SQLPAGE_REDIRECT_URI : & str = "/sqlpage/oidc_callback" ;
4343const SQLPAGE_NONCE_COOKIE_NAME : & str = "sqlpage_oidc_nonce" ;
44- const SQLPAGE_REDIRECT_URL_COOKIE_PREFIX : & str = "sqlpage_oidc_redirect_url_ " ;
44+ const SQLPAGE_TMP_LOGIN_STATE_COOKIE_PREFIX : & str = "sqlpage_oidc_state_ " ;
4545const OIDC_CLIENT_MAX_REFRESH_INTERVAL : Duration = Duration :: from_secs ( 60 * 60 ) ;
4646const OIDC_CLIENT_MIN_REFRESH_INTERVAL : Duration = Duration :: from_secs ( 5 ) ;
4747const AUTH_COOKIE_EXPIRATION : awc:: cookie:: time:: Duration =
4848 actix_web:: cookie:: time:: Duration :: days ( 7 ) ;
49+ const LOGIN_FLOW_STATE_COOKIE_EXPIRATION : awc:: cookie:: time:: Duration =
50+ actix_web:: cookie:: time:: Duration :: minutes ( 10 ) ;
4951
5052#[ derive( Clone , Debug , Serialize , Deserialize ) ]
5153#[ serde( transparent) ]
@@ -329,15 +331,17 @@ enum MiddlewareResponse {
329331async fn handle_request ( oidc_state : & OidcState , request : ServiceRequest ) -> MiddlewareResponse {
330332 log:: trace!( "Started OIDC middleware request handling" ) ;
331333 oidc_state. refresh_if_expired ( & request) . await ;
334+
335+ if request. path ( ) == SQLPAGE_REDIRECT_URI {
336+ let response = handle_oidc_callback ( oidc_state, request) . await ;
337+ return MiddlewareResponse :: Respond ( response) ;
338+ }
339+
332340 match get_authenticated_user_info ( oidc_state, & request) . await {
333341 Ok ( Some ( claims) ) => {
334- if request. path ( ) == SQLPAGE_REDIRECT_URI {
335- MiddlewareResponse :: Respond ( handle_authenticated_oidc_callback ( request) )
336- } else {
337- log:: trace!( "Storing authenticated user info in request extensions: {claims:?}" ) ;
338- request. extensions_mut ( ) . insert ( claims) ;
339- MiddlewareResponse :: Forward ( request)
340- }
342+ log:: trace!( "Storing authenticated user info in request extensions: {claims:?}" ) ;
343+ request. extensions_mut ( ) . insert ( claims) ;
344+ MiddlewareResponse :: Forward ( request)
341345 }
342346 Ok ( None ) => {
343347 log:: trace!( "No authenticated user found" ) ;
@@ -356,11 +360,6 @@ async fn handle_unauthenticated_request(
356360 request : ServiceRequest ,
357361) -> MiddlewareResponse {
358362 log:: debug!( "Handling unauthenticated request to {}" , request. path( ) ) ;
359- if request. path ( ) == SQLPAGE_REDIRECT_URI {
360- log:: debug!( "The request is the OIDC callback" ) ;
361- let response = handle_oidc_callback ( oidc_state, request) . await ;
362- return MiddlewareResponse :: Respond ( response) ;
363- }
364363
365364 if oidc_state. config . is_public_path ( request. path ( ) ) {
366365 return MiddlewareResponse :: Forward ( request) ;
@@ -374,8 +373,7 @@ async fn handle_unauthenticated_request(
374373}
375374
376375async fn handle_oidc_callback ( oidc_state : & OidcState , request : ServiceRequest ) -> ServiceResponse {
377- let query_string = request. query_string ( ) ;
378- match process_oidc_callback ( oidc_state, query_string, & request) . await {
376+ match process_oidc_callback ( oidc_state, & request) . await {
379377 Ok ( response) => request. into_response ( response) ,
380378 Err ( e) => {
381379 log:: error!( "Failed to process OIDC callback. Refreshing oidc provider metadata, then redirecting to home page: {e:#}" ) ;
@@ -386,24 +384,6 @@ async fn handle_oidc_callback(oidc_state: &OidcState, request: ServiceRequest) -
386384 }
387385}
388386
389- /// When an user has already authenticated (potentially in another tab), we ignore the callback and redirect to the initial URL.
390- fn handle_authenticated_oidc_callback ( request : ServiceRequest ) -> ServiceResponse {
391- // Try to get redirect URL from query params if available
392- let redirect_url = Query :: < OidcCallbackParams > :: from_query ( request. query_string ( ) )
393- . with_context ( || "Failed to parse OIDC callback parameters in authenticated callback" )
394- . and_then ( |params| get_redirect_url_cookie ( & request, & params. state ) )
395- . map_or_else (
396- |e| {
397- log:: warn!( "No redirect URL cookie: {e:#}" ) ;
398- "/" . to_string ( )
399- } ,
400- |cookie| cookie. value ( ) . to_string ( ) ,
401- ) ;
402-
403- log:: debug!( "OIDC callback received for authenticated user. Redirecting to {redirect_url}" ) ;
404- request. into_response ( build_redirect_response ( redirect_url) )
405- }
406-
407387impl < S > Service < ServiceRequest > for OidcService < S >
408388where
409389 S : Service < ServiceRequest , Response = ServiceResponse < BoxBody > , Error = Error > + ' static ,
@@ -429,64 +409,59 @@ where
429409
430410async fn process_oidc_callback (
431411 oidc_state : & OidcState ,
432- query_string : & str ,
433412 request : & ServiceRequest ,
434413) -> anyhow:: Result < HttpResponse > {
435- let http_client = get_http_client_from_appdata ( request) ?;
436-
437- let params = Query :: < OidcCallbackParams > :: from_query ( query_string)
438- . with_context ( || {
439- format ! (
440- "{SQLPAGE_REDIRECT_URI}: failed to parse OIDC callback parameters from {query_string}"
441- )
442- } ) ?
414+ let params = Query :: < OidcCallbackParams > :: from_query ( request. query_string ( ) )
415+ . with_context ( || format ! ( "{SQLPAGE_REDIRECT_URI}: invalid url parameters" ) ) ?
443416 . into_inner ( ) ;
444-
445- let mut redirect_url_cookie = get_redirect_url_cookie ( request, & params. state )
446- . with_context ( || "Failed to read redirect URL from cookie" ) ?;
447-
448- let client = oidc_state. get_client ( ) . await ;
449417 log:: debug!( "Processing OIDC callback with params: {params:?}. Requesting token..." ) ;
450- let token_response = exchange_code_for_token ( & client, http_client, params. clone ( ) ) . await ?;
451- log:: debug!( "Received token response: {token_response:?}" ) ;
418+ let mut tmp_login_flow_state_cookie = get_tmp_login_flow_state_cookie ( request, & params. state ) ?;
419+ let client = oidc_state. get_client ( ) . await ;
420+ let http_client = get_http_client_from_appdata ( request) ?;
421+ let id_token = exchange_code_for_token ( & client, http_client, params. clone ( ) ) . await ?;
422+ log:: debug!( "Received token response: {id_token:?}" ) ;
423+ let LoginFlowState {
424+ nonce,
425+ redirect_target,
426+ } = parse_login_flow_state ( & tmp_login_flow_state_cookie) ?;
427+ let redirect_target = validate_redirect_url ( redirect_target. to_string ( ) ) ;
452428
453- let redirect_target = validate_redirect_url ( redirect_url_cookie. value ( ) . to_string ( ) ) ;
454429 log:: info!( "Redirecting to {redirect_target} after a successful login" ) ;
455430 let mut response = build_redirect_response ( redirect_target) ;
456- set_auth_cookie ( & mut response, & token_response) . context ( "Failed to set auth cookie" ) ?;
457-
458- // Clean up the state-specific cookie after successful authentication
459- redirect_url_cookie. set_path ( "/" ) ;
460- response. add_removal_cookie ( & redirect_url_cookie) ?;
461-
431+ set_auth_cookie ( & mut response, & id_token) ;
432+ let claims = oidc_state
433+ . get_token_claims ( id_token, & nonce)
434+ . await
435+ . context ( "The identity provider returned an invalid ID token" ) ?;
436+ log:: debug!( "{} successfully logged in" , claims. subject( ) . as_str( ) ) ;
437+ let nonce_cookie = create_final_nonce_cookie ( & nonce) ;
438+ response. add_cookie ( & nonce_cookie) ?;
439+ tmp_login_flow_state_cookie. set_path ( "/" ) ; // Required to clean up the cookie
440+ response. add_removal_cookie ( & tmp_login_flow_state_cookie) ?;
462441 Ok ( response)
463442}
464443
465444async fn exchange_code_for_token (
466445 oidc_client : & OidcClient ,
467446 http_client : & awc:: Client ,
468447 oidc_callback_params : OidcCallbackParams ,
469- ) -> anyhow:: Result < OidcTokenResponse > {
448+ ) -> anyhow:: Result < OidcToken > {
470449 let token_response = oidc_client
471450 . exchange_code ( openidconnect:: AuthorizationCode :: new (
472451 oidc_callback_params. code ,
473452 ) ) ?
474453 . request_async ( & AwcHttpClient :: from_client ( http_client) )
475454 . await
476455 . context ( "Failed to exchange code for token" ) ?;
477- Ok ( token_response)
478- }
479-
480- fn set_auth_cookie (
481- response : & mut HttpResponse ,
482- token_response : & OidcTokenResponse ,
483- ) -> anyhow:: Result < ( ) > {
484456 let access_token = token_response. access_token ( ) ;
485457 log:: trace!( "Received access token: {}" , access_token. secret( ) ) ;
486458 let id_token = token_response
487459 . id_token ( )
488460 . context ( "No ID token found in the token response. You may have specified an oauth2 provider that does not support OIDC." ) ?;
461+ Ok ( id_token. clone ( ) )
462+ }
489463
464+ fn set_auth_cookie ( response : & mut HttpResponse , id_token : & OidcToken ) {
490465 let id_token_str = id_token. to_string ( ) ;
491466 log:: trace!( "Setting auth cookie: {SQLPAGE_AUTH_COOKIE_NAME}=\" {id_token_str}\" " ) ;
492467 let id_token_size_kb = id_token_str. len ( ) / 1024 ;
@@ -505,20 +480,17 @@ fn set_auth_cookie(
505480 . finish ( ) ;
506481
507482 response. add_cookie ( & cookie) . unwrap ( ) ;
508- Ok ( ( ) )
509483}
510484
511485async fn build_auth_provider_redirect_response (
512486 oidc_state : & OidcState ,
513487 initial_url : & str ,
514488) -> HttpResponse {
515489 let AuthUrl { url, params } = build_auth_url ( oidc_state) . await ;
516- let nonce_cookie = create_nonce_cookie ( & params. nonce ) ;
517- let redirect_cookie = create_redirect_cookie ( & params. csrf_token , initial_url) ;
490+ let tmp_login_flow_state_cookie = create_tmp_login_flow_state_cookie ( & params, initial_url) ;
518491 HttpResponse :: TemporaryRedirect ( )
519492 . append_header ( ( header:: LOCATION , url. to_string ( ) ) )
520- . cookie ( nonce_cookie)
521- . cookie ( redirect_cookie)
493+ . cookie ( tmp_login_flow_state_cookie)
522494 . body ( "Redirecting..." )
523495}
524496
@@ -540,7 +512,7 @@ async fn get_authenticated_user_info(
540512 let id_token = OidcToken :: from_str ( & cookie_value)
541513 . with_context ( || format ! ( "Invalid SQLPage auth cookie: {cookie_value:?}" ) ) ?;
542514
543- let nonce = get_nonce_from_cookie ( request) ?;
515+ let nonce = get_final_nonce_from_cookie ( request) ?;
544516 log:: debug!( "Verifying id token: {id_token:?}" ) ;
545517 let claims = oidc_state. get_token_claims ( id_token, & nonce) . await ?;
546518 log:: debug!( "The current user is: {claims:?}" ) ;
@@ -772,7 +744,7 @@ fn nonce_matches(id_token_nonce: &Nonce, state_nonce: &Nonce) -> Result<(), Stri
772744 Ok ( ( ) )
773745}
774746
775- fn create_nonce_cookie ( nonce : & Nonce ) -> Cookie < ' _ > {
747+ fn create_final_nonce_cookie ( nonce : & Nonce ) -> Cookie < ' _ > {
776748 Cookie :: build ( SQLPAGE_NONCE_COOKIE_NAME , nonce. secret ( ) )
777749 . secure ( true )
778750 . http_only ( true )
@@ -782,34 +754,56 @@ fn create_nonce_cookie(nonce: &Nonce) -> Cookie<'_> {
782754 . finish ( )
783755}
784756
785- fn create_redirect_cookie < ' a > ( csrf_token : & CsrfToken , initial_url : & ' a str ) -> Cookie < ' a > {
786- let cookie_name = SQLPAGE_REDIRECT_URL_COOKIE_PREFIX . to_owned ( ) + csrf_token. secret ( ) ;
787- Cookie :: build ( cookie_name, initial_url)
757+ fn create_tmp_login_flow_state_cookie < ' a > (
758+ params : & ' a AuthUrlParams ,
759+ initial_url : & ' a str ,
760+ ) -> Cookie < ' a > {
761+ let csrf_token = & params. csrf_token ;
762+ let cookie_name = SQLPAGE_TMP_LOGIN_STATE_COOKIE_PREFIX . to_owned ( ) + csrf_token. secret ( ) ;
763+ let cookie_value = serde_json:: to_string ( & LoginFlowState {
764+ nonce : params. nonce . clone ( ) ,
765+ redirect_target : initial_url,
766+ } )
767+ . expect ( "login flow state is always serializable" ) ;
768+ Cookie :: build ( cookie_name, cookie_value)
788769 . secure ( true )
789770 . http_only ( true )
790771 . same_site ( actix_web:: cookie:: SameSite :: Lax )
791772 . path ( "/" )
792- . max_age ( actix_web :: cookie :: time :: Duration :: minutes ( 10 ) )
773+ . max_age ( LOGIN_FLOW_STATE_COOKIE_EXPIRATION )
793774 . finish ( )
794775}
795776
796- fn get_nonce_from_cookie ( request : & ServiceRequest ) -> anyhow:: Result < Nonce > {
777+ fn get_final_nonce_from_cookie ( request : & ServiceRequest ) -> anyhow:: Result < Nonce > {
797778 let cookie = request
798779 . cookie ( SQLPAGE_NONCE_COOKIE_NAME )
799780 . with_context ( || format ! ( "No {SQLPAGE_NONCE_COOKIE_NAME} cookie found" ) ) ?;
800781 Ok ( Nonce :: new ( cookie. value ( ) . to_string ( ) ) )
801782}
802783
803- fn get_redirect_url_cookie (
784+ fn get_tmp_login_flow_state_cookie (
804785 request : & ServiceRequest ,
805786 csrf_token : & CsrfToken ,
806787) -> anyhow:: Result < Cookie < ' static > > {
807- let cookie_name = SQLPAGE_REDIRECT_URL_COOKIE_PREFIX . to_owned ( ) + csrf_token. secret ( ) ;
788+ let cookie_name = SQLPAGE_TMP_LOGIN_STATE_COOKIE_PREFIX . to_owned ( ) + csrf_token. secret ( ) ;
808789 request
809790 . cookie ( & cookie_name)
810791 . with_context ( || format ! ( "No {cookie_name} cookie found" ) )
811792}
812793
794+ #[ derive( Debug , Serialize , Deserialize , Clone ) ]
795+ struct LoginFlowState < ' a > {
796+ #[ serde( rename = "n" ) ]
797+ nonce : Nonce ,
798+ #[ serde( rename = "r" ) ]
799+ redirect_target : & ' a str ,
800+ }
801+
802+ fn parse_login_flow_state < ' a > ( cookie : & ' a Cookie < ' _ > ) -> anyhow:: Result < LoginFlowState < ' a > > {
803+ serde_json:: from_str ( cookie. value ( ) )
804+ . with_context ( || format ! ( "Invalid login flow state cookie: {}" , cookie. value( ) ) )
805+ }
806+
813807/// Given an audience, verify if it is trusted. The `client_id` is always trusted, independently of this function.
814808#[ derive( Clone , Debug ) ]
815809pub struct AudienceVerifier ( Option < HashSet < String > > ) ;
0 commit comments