@@ -40,7 +40,8 @@ type LocalBoxFuture<T> = Pin<Box<dyn Future<Output = T> + 'static>>;
4040
4141const SQLPAGE_AUTH_COOKIE_NAME : & str = "sqlpage_auth" ;
4242const SQLPAGE_REDIRECT_URI : & str = "/sqlpage/oidc_callback" ;
43- const SQLPAGE_STATE_COOKIE_NAME : & str = "sqlpage_oidc_state" ;
43+ const SQLPAGE_NONCE_COOKIE_NAME : & str = "sqlpage_oidc_nonce" ;
44+ const SQLPAGE_STATE_COOKIE_PREFIX : & str = "sqlpage_oidc_state_" ;
4445const OIDC_CLIENT_MAX_REFRESH_INTERVAL : Duration = Duration :: from_secs ( 60 * 60 ) ;
4546const OIDC_CLIENT_MIN_REFRESH_INTERVAL : Duration = Duration :: from_secs ( 5 ) ;
4647
@@ -375,8 +376,17 @@ async fn handle_oidc_callback(oidc_state: &OidcState, request: ServiceRequest) -
375376 match process_oidc_callback ( oidc_state, query_string, & request) . await {
376377 Ok ( response) => request. into_response ( response) ,
377378 Err ( e) => {
379+ // Try to get redirect URL from query params if available
378380 let redirect_url =
379- get_state_from_cookie ( & request) . map_or_else ( |_| "/" . into ( ) , |s| s. initial_url ) ;
381+ if let Ok ( params) = Query :: < OidcCallbackParams > :: from_query ( query_string) {
382+ if let Ok ( state) = get_state_from_cookies ( & request, & params. state ) {
383+ state. initial_url
384+ } else {
385+ "/" . to_string ( )
386+ }
387+ } else {
388+ "/" . to_string ( )
389+ } ;
380390 log:: error!( "Failed to process OIDC callback. Refreshing oidc provider metadata, then redirecting to {redirect_url}: {e:#}" ) ;
381391 oidc_state. refresh_on_error ( & request) . await ;
382392 let resp = build_auth_provider_redirect_response ( oidc_state, redirect_url) . await ;
@@ -387,10 +397,17 @@ async fn handle_oidc_callback(oidc_state: &OidcState, request: ServiceRequest) -
387397
388398/// When an user has already authenticated (potentially in another tab), we ignore the callback and redirect to the initial URL.
389399fn handle_authenticated_oidc_callback ( request : ServiceRequest ) -> ServiceResponse {
390- let redirect_url = match get_state_from_cookie ( & request) {
391- Ok ( state) => state. initial_url ,
392- Err ( _) => "/" . to_string ( ) ,
393- } ;
400+ // Try to get redirect URL from query params if available
401+ let redirect_url =
402+ if let Ok ( params) = Query :: < OidcCallbackParams > :: from_query ( request. query_string ( ) ) {
403+ if let Ok ( state) = get_state_from_cookies ( & request, & params. state ) {
404+ state. initial_url
405+ } else {
406+ "/" . to_string ( )
407+ }
408+ } else {
409+ "/" . to_string ( )
410+ } ;
394411 log:: debug!( "OIDC callback received for authenticated user. Redirecting to {redirect_url}" ) ;
395412 request. into_response ( build_redirect_response ( redirect_url) )
396413}
@@ -425,8 +442,6 @@ async fn process_oidc_callback(
425442) -> anyhow:: Result < HttpResponse > {
426443 let http_client = get_http_client_from_appdata ( request) ?;
427444
428- let state = get_state_from_cookie ( request) . context ( "Failed to read oidc state cookie" ) ?;
429-
430445 let params = Query :: < OidcCallbackParams > :: from_query ( query_string)
431446 . with_context ( || {
432447 format ! (
@@ -435,10 +450,8 @@ async fn process_oidc_callback(
435450 } ) ?
436451 . into_inner ( ) ;
437452
438- if state. csrf_token . secret ( ) != params. state . secret ( ) {
439- log:: debug!( "CSRF token mismatch: expected {state:?}, got {params:?}" ) ;
440- return Err ( anyhow ! ( "Invalid CSRF token: {}" , params. state. secret( ) ) ) ;
441- }
453+ let state = get_state_from_cookies ( request, & params. state )
454+ . context ( "Failed to read oidc state cookies" ) ?;
442455
443456 let client = oidc_state. get_client ( ) . await ;
444457 log:: debug!( "Processing OIDC callback with params: {params:?}. Requesting token..." ) ;
@@ -451,6 +464,22 @@ async fn process_oidc_callback(
451464 set_auth_cookie ( & mut response, & token_response, oidc_state)
452465 . await
453466 . context ( "Failed to set auth cookie" ) ?;
467+
468+ // Clean up the state-specific cookie after successful authentication
469+ let state_cookie_name = format ! (
470+ "{}{}" ,
471+ SQLPAGE_STATE_COOKIE_PREFIX ,
472+ state. csrf_token. secret( )
473+ ) ;
474+ let cleanup_cookie = Cookie :: build ( state_cookie_name, "" )
475+ . secure ( true )
476+ . http_only ( true )
477+ . same_site ( actix_web:: cookie:: SameSite :: Lax )
478+ . path ( "/" )
479+ . max_age ( actix_web:: cookie:: time:: Duration :: seconds ( 0 ) ) // Expire immediately
480+ . finish ( ) ;
481+ response. add_cookie ( & cleanup_cookie) . unwrap ( ) ;
482+
454483 Ok ( response)
455484}
456485
@@ -511,10 +540,11 @@ async fn build_auth_provider_redirect_response(
511540) -> HttpResponse {
512541 let AuthUrl { url, params } = build_auth_url ( oidc_state) . await ;
513542 let state = OidcLoginState :: new ( initial_url, params) ;
514- let state_cookie = create_state_cookie ( & state) ;
543+ let ( nonce_cookie , redirect_cookie ) = create_state_cookies ( & state) ;
515544 HttpResponse :: TemporaryRedirect ( )
516545 . append_header ( ( "Location" , url. to_string ( ) ) )
517- . cookie ( state_cookie)
546+ . cookie ( nonce_cookie)
547+ . cookie ( redirect_cookie)
518548 . body ( "Redirecting..." )
519549}
520550
@@ -536,9 +566,21 @@ async fn get_authenticated_user_info(
536566 let id_token = OidcToken :: from_str ( & cookie_value)
537567 . with_context ( || format ! ( "Invalid SQLPage auth cookie: {cookie_value:?}" ) ) ?;
538568
539- let state = get_state_from_cookie ( request) ?;
569+ // Try to get state from cookies if this is a callback request
570+ let state = if request. path ( ) == SQLPAGE_REDIRECT_URI {
571+ if let Ok ( params) = Query :: < OidcCallbackParams > :: from_query ( request. query_string ( ) ) {
572+ get_state_from_cookies ( request, & params. state ) . ok ( )
573+ } else {
574+ None
575+ }
576+ } else {
577+ None
578+ } ;
579+
540580 log:: debug!( "Verifying id token: {id_token:?}" ) ;
541- let claims = oidc_state. get_token_claims ( id_token, Some ( & state) ) . await ?;
581+ let claims = oidc_state
582+ . get_token_claims ( id_token, state. as_ref ( ) )
583+ . await ?;
542584 log:: debug!( "The current user is: {claims:?}" ) ;
543585 Ok ( Some ( claims) )
544586}
@@ -788,6 +830,21 @@ struct OidcLoginState {
788830 nonce : Nonce ,
789831}
790832
833+ #[ derive( Debug , Serialize , Deserialize ) ]
834+ struct OidcNonceState {
835+ /// The source nonce to use for the login process. It must be checked against the hash
836+ /// stored in the ID token.
837+ #[ serde( rename = "n" ) ]
838+ nonce : Nonce ,
839+ }
840+
841+ #[ derive( Debug , Serialize , Deserialize ) ]
842+ struct OidcRedirectState {
843+ /// The URL to redirect to after the login process is complete.
844+ #[ serde( rename = "u" ) ]
845+ initial_url : String ,
846+ }
847+
791848impl OidcLoginState {
792849 fn new ( initial_url : String , auth_url : AuthUrlParams ) -> Self {
793850 Self {
@@ -798,22 +855,68 @@ impl OidcLoginState {
798855 }
799856}
800857
801- fn create_state_cookie ( login_state : & OidcLoginState ) -> Cookie < ' _ > {
802- let state_json = serde_json:: to_string ( login_state) . unwrap ( ) ;
803- Cookie :: build ( SQLPAGE_STATE_COOKIE_NAME , state_json)
858+ fn create_state_cookies ( login_state : & OidcLoginState ) -> ( Cookie < ' _ > , Cookie < ' _ > ) {
859+ // Create nonce cookie (longer-lived for security)
860+ let nonce_state = OidcNonceState {
861+ nonce : login_state. nonce . clone ( ) ,
862+ } ;
863+ let nonce_json = serde_json:: to_string ( & nonce_state) . unwrap ( ) ;
864+ let nonce_cookie = Cookie :: build ( SQLPAGE_NONCE_COOKIE_NAME , nonce_json)
865+ . secure ( true )
866+ . http_only ( true )
867+ . same_site ( actix_web:: cookie:: SameSite :: Lax )
868+ . path ( "/" )
869+ . max_age ( actix_web:: cookie:: time:: Duration :: minutes ( 10 ) ) // 10 minutes should be enough for login flow
870+ . finish ( ) ;
871+
872+ // Create state-specific redirect cookie (short-lived)
873+ let redirect_state = OidcRedirectState {
874+ initial_url : login_state. initial_url . clone ( ) ,
875+ } ;
876+ let redirect_json = serde_json:: to_string ( & redirect_state) . unwrap ( ) ;
877+ let state_cookie_name = format ! (
878+ "{}{}" ,
879+ SQLPAGE_STATE_COOKIE_PREFIX ,
880+ login_state. csrf_token. secret( )
881+ ) ;
882+ let redirect_cookie = Cookie :: build ( state_cookie_name, redirect_json)
804883 . secure ( true )
805884 . http_only ( true )
806885 . same_site ( actix_web:: cookie:: SameSite :: Lax )
807886 . path ( "/" )
808- . finish ( )
887+ . max_age ( actix_web:: cookie:: time:: Duration :: minutes ( 5 ) ) // Short-lived, only needed during login flow
888+ . finish ( ) ;
889+
890+ ( nonce_cookie, redirect_cookie)
809891}
810892
811- fn get_state_from_cookie ( request : & ServiceRequest ) -> anyhow:: Result < OidcLoginState > {
812- let state_cookie = request. cookie ( SQLPAGE_STATE_COOKIE_NAME ) . with_context ( || {
813- format ! ( "No {SQLPAGE_STATE_COOKIE_NAME} cookie found for {SQLPAGE_REDIRECT_URI}" )
893+ fn get_state_from_cookies (
894+ request : & ServiceRequest ,
895+ csrf_token : & CsrfToken ,
896+ ) -> anyhow:: Result < OidcLoginState > {
897+ // Get nonce from the nonce cookie
898+ let nonce_cookie = request. cookie ( SQLPAGE_NONCE_COOKIE_NAME ) . with_context ( || {
899+ format ! ( "No {SQLPAGE_NONCE_COOKIE_NAME} cookie found for {SQLPAGE_REDIRECT_URI}" )
814900 } ) ?;
815- serde_json:: from_str ( state_cookie. value ( ) )
816- . with_context ( || format ! ( "Failed to parse OIDC state from cookie: {state_cookie}" ) )
901+ let nonce_state: OidcNonceState = serde_json:: from_str ( nonce_cookie. value ( ) )
902+ . with_context ( || format ! ( "Failed to parse OIDC nonce from cookie: {nonce_cookie}" ) ) ?;
903+
904+ // Get redirect URL from the state-specific cookie
905+ let state_cookie_name = format ! ( "{}{}" , SQLPAGE_STATE_COOKIE_PREFIX , csrf_token. secret( ) ) ;
906+ let redirect_cookie = request. cookie ( & state_cookie_name) . with_context ( || {
907+ format ! ( "No {state_cookie_name} cookie found for {SQLPAGE_REDIRECT_URI}" )
908+ } ) ?;
909+ let redirect_state: OidcRedirectState = serde_json:: from_str ( redirect_cookie. value ( ) )
910+ . with_context ( || {
911+ format ! ( "Failed to parse OIDC redirect state from cookie: {redirect_cookie}" )
912+ } ) ?;
913+
914+ // Reconstruct the full login state
915+ Ok ( OidcLoginState {
916+ initial_url : redirect_state. initial_url ,
917+ csrf_token : csrf_token. clone ( ) ,
918+ nonce : nonce_state. nonce ,
919+ } )
817920}
818921
819922/// Given an audience, verify if it is trusted. The `client_id` is always trusted, independently of this function.
0 commit comments