@@ -6,6 +6,7 @@ use std::{future::Future, pin::Pin, str::FromStr, sync::Arc};
66
77use crate :: webserver:: http_client:: get_http_client_from_appdata;
88use crate :: { app_config:: AppConfig , AppState } ;
9+ use actix_web:: http:: header;
910use actix_web:: {
1011 body:: BoxBody ,
1112 cookie:: Cookie ,
@@ -16,7 +17,6 @@ use actix_web::{
1617} ;
1718use anyhow:: { anyhow, Context } ;
1819use awc:: Client ;
19- use chrono:: Utc ;
2020use openidconnect:: core:: {
2121 CoreAuthDisplay , CoreAuthPrompt , CoreErrorResponseType , CoreGenderClaim , CoreJsonWebKey ,
2222 CoreJweContentEncryptionAlgorithm , CoreJwsSigningAlgorithm , CoreRevocableToken ,
@@ -40,9 +40,12 @@ 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_REDIRECT_URL_COOKIE_PREFIX : & str = "sqlpage_oidc_redirect_url_" ;
4445const OIDC_CLIENT_MAX_REFRESH_INTERVAL : Duration = Duration :: from_secs ( 60 * 60 ) ;
4546const OIDC_CLIENT_MIN_REFRESH_INTERVAL : Duration = Duration :: from_secs ( 5 ) ;
47+ const AUTH_COOKIE_EXPIRATION : awc:: cookie:: time:: Duration =
48+ actix_web:: cookie:: time:: Duration :: days ( 7 ) ;
4649
4750#[ derive( Clone , Debug , Serialize , Deserialize ) ]
4851#[ serde( transparent) ]
@@ -209,11 +212,11 @@ impl OidcState {
209212 async fn get_token_claims (
210213 & self ,
211214 id_token : OidcToken ,
212- state : Option < & OidcLoginState > ,
215+ expected_nonce : & Nonce ,
213216 ) -> anyhow:: Result < OidcClaims > {
214217 let client = & self . get_client ( ) . await ;
215218 let verifier = self . config . create_id_token_verifier ( client) ;
216- let nonce_verifier = |nonce : Option < & Nonce > | check_nonce ( nonce, state ) ;
219+ let nonce_verifier = |nonce : Option < & Nonce > | check_nonce ( nonce, expected_nonce ) ;
217220 let claims: OidcClaims = id_token
218221 . into_claims ( & verifier, nonce_verifier)
219222 . map_err ( |e| anyhow:: anyhow!( "Could not verify the ID token: {}" , e) ) ?;
@@ -366,7 +369,7 @@ async fn handle_unauthenticated_request(
366369 log:: debug!( "Redirecting to OIDC provider" ) ;
367370
368371 let initial_url = request. uri ( ) . to_string ( ) ;
369- let response = build_auth_provider_redirect_response ( oidc_state, initial_url) . await ;
372+ let response = build_auth_provider_redirect_response ( oidc_state, & initial_url) . await ;
370373 MiddlewareResponse :: Respond ( request. into_response ( response) )
371374}
372375
@@ -375,22 +378,28 @@ async fn handle_oidc_callback(oidc_state: &OidcState, request: ServiceRequest) -
375378 match process_oidc_callback ( oidc_state, query_string, & request) . await {
376379 Ok ( response) => request. into_response ( response) ,
377380 Err ( e) => {
378- let redirect_url =
379- get_state_from_cookie ( & request) . map_or_else ( |_| "/" . into ( ) , |s| s. initial_url ) ;
380- log:: error!( "Failed to process OIDC callback. Refreshing oidc provider metadata, then redirecting to {redirect_url}: {e:#}" ) ;
381+ log:: error!( "Failed to process OIDC callback. Refreshing oidc provider metadata, then redirecting to home page: {e:#}" ) ;
381382 oidc_state. refresh_on_error ( & request) . await ;
382- let resp = build_auth_provider_redirect_response ( oidc_state, redirect_url ) . await ;
383+ let resp = build_auth_provider_redirect_response ( oidc_state, "/" ) . await ;
383384 request. into_response ( resp)
384385 }
385386 }
386387}
387388
388389/// When an user has already authenticated (potentially in another tab), we ignore the callback and redirect to the initial URL.
389390fn 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- } ;
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+
394403 log:: debug!( "OIDC callback received for authenticated user. Redirecting to {redirect_url}" ) ;
395404 request. into_response ( build_redirect_response ( redirect_url) )
396405}
@@ -425,8 +434,6 @@ async fn process_oidc_callback(
425434) -> anyhow:: Result < HttpResponse > {
426435 let http_client = get_http_client_from_appdata ( request) ?;
427436
428- let state = get_state_from_cookie ( request) . context ( "Failed to read oidc state cookie" ) ?;
429-
430437 let params = Query :: < OidcCallbackParams > :: from_query ( query_string)
431438 . with_context ( || {
432439 format ! (
@@ -435,22 +442,23 @@ async fn process_oidc_callback(
435442 } ) ?
436443 . into_inner ( ) ;
437444
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- }
445+ let mut redirect_url_cookie = get_redirect_url_cookie ( request, & params. state )
446+ . with_context ( || "Failed to read redirect URL from cookie" ) ?;
442447
443448 let client = oidc_state. get_client ( ) . await ;
444449 log:: debug!( "Processing OIDC callback with params: {params:?}. Requesting token..." ) ;
445- let token_response = exchange_code_for_token ( & client, http_client, params) . await ?;
450+ let token_response = exchange_code_for_token ( & client, http_client, params. clone ( ) ) . await ?;
446451 log:: debug!( "Received token response: {token_response:?}" ) ;
447452
448- let redirect_target = validate_redirect_url ( state . initial_url ) ;
453+ let redirect_target = validate_redirect_url ( redirect_url_cookie . value ( ) . to_string ( ) ) ;
449454 log:: info!( "Redirecting to {redirect_target} after a successful login" ) ;
450455 let mut response = build_redirect_response ( redirect_target) ;
451- set_auth_cookie ( & mut response, & token_response, oidc_state)
452- . await
453- . context ( "Failed to set auth cookie" ) ?;
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+
454462 Ok ( response)
455463}
456464
@@ -469,21 +477,16 @@ async fn exchange_code_for_token(
469477 Ok ( token_response)
470478}
471479
472- async fn set_auth_cookie (
480+ fn set_auth_cookie (
473481 response : & mut HttpResponse ,
474482 token_response : & OidcTokenResponse ,
475- oidc_state : & OidcState ,
476483) -> anyhow:: Result < ( ) > {
477484 let access_token = token_response. access_token ( ) ;
478485 log:: trace!( "Received access token: {}" , access_token. secret( ) ) ;
479486 let id_token = token_response
480487 . id_token ( )
481488 . context ( "No ID token found in the token response. You may have specified an oauth2 provider that does not support OIDC." ) ?;
482489
483- let claims_res = oidc_state. get_token_claims ( id_token. clone ( ) , None ) . await ;
484- let expiration = claims_res. context ( "Parsing ID token claims" ) ?. expiration ( ) ;
485- let max_age_seconds = expiration. signed_duration_since ( Utc :: now ( ) ) . num_seconds ( ) ;
486-
487490 let id_token_str = id_token. to_string ( ) ;
488491 log:: trace!( "Setting auth cookie: {SQLPAGE_AUTH_COOKIE_NAME}=\" {id_token_str}\" " ) ;
489492 let id_token_size_kb = id_token_str. len ( ) / 1024 ;
@@ -496,7 +499,7 @@ async fn set_auth_cookie(
496499 let cookie = Cookie :: build ( SQLPAGE_AUTH_COOKIE_NAME , id_token_str)
497500 . secure ( true )
498501 . http_only ( true )
499- . max_age ( actix_web :: cookie :: time :: Duration :: seconds ( max_age_seconds ) )
502+ . max_age ( AUTH_COOKIE_EXPIRATION )
500503 . same_site ( actix_web:: cookie:: SameSite :: Lax )
501504 . path ( "/" )
502505 . finish ( ) ;
@@ -507,14 +510,15 @@ async fn set_auth_cookie(
507510
508511async fn build_auth_provider_redirect_response (
509512 oidc_state : & OidcState ,
510- initial_url : String ,
513+ initial_url : & str ,
511514) -> HttpResponse {
512515 let AuthUrl { url, params } = build_auth_url ( oidc_state) . await ;
513- let state = OidcLoginState :: new ( initial_url , params) ;
514- let state_cookie = create_state_cookie ( & state ) ;
516+ let nonce_cookie = create_nonce_cookie ( & params. nonce ) ;
517+ let redirect_cookie = create_redirect_cookie ( & params . csrf_token , initial_url ) ;
515518 HttpResponse :: TemporaryRedirect ( )
516- . append_header ( ( "Location" , url. to_string ( ) ) )
517- . cookie ( state_cookie)
519+ . append_header ( ( header:: LOCATION , url. to_string ( ) ) )
520+ . cookie ( nonce_cookie)
521+ . cookie ( redirect_cookie)
518522 . body ( "Redirecting..." )
519523}
520524
@@ -536,9 +540,9 @@ async fn get_authenticated_user_info(
536540 let id_token = OidcToken :: from_str ( & cookie_value)
537541 . with_context ( || format ! ( "Invalid SQLPage auth cookie: {cookie_value:?}" ) ) ?;
538542
539- let state = get_state_from_cookie ( request) ?;
543+ let nonce = get_nonce_from_cookie ( request) ?;
540544 log:: debug!( "Verifying id token: {id_token:?}" ) ;
541- let claims = oidc_state. get_token_claims ( id_token, Some ( & state ) ) . await ?;
545+ let claims = oidc_state. get_token_claims ( id_token, & nonce ) . await ?;
542546 log:: debug!( "The current user is: {claims:?}" ) ;
543547 Ok ( Some ( claims) )
544548}
@@ -689,7 +693,7 @@ fn make_oidc_client(
689693 Ok ( client)
690694}
691695
692- #[ derive( Debug , Deserialize ) ]
696+ #[ derive( Debug , Deserialize , Clone ) ]
693697struct OidcCallbackParams {
694698 code : String ,
695699 state : CsrfToken ,
@@ -739,15 +743,9 @@ fn hash_nonce(nonce: &Nonce) -> String {
739743 hash. to_string ( )
740744}
741745
742- fn check_nonce (
743- id_token_nonce : Option < & Nonce > ,
744- login_state : Option < & OidcLoginState > ,
745- ) -> Result < ( ) , String > {
746- let Some ( state) = login_state else {
747- return Ok ( ( ) ) ; // No login state, no nonce to check
748- } ;
746+ fn check_nonce ( id_token_nonce : Option < & Nonce > , expected_nonce : & Nonce ) -> Result < ( ) , String > {
749747 match id_token_nonce {
750- Some ( id_token_nonce) => nonce_matches ( id_token_nonce, & state . nonce ) ,
748+ Some ( id_token_nonce) => nonce_matches ( id_token_nonce, expected_nonce ) ,
751749 None => Err ( "No nonce found in the ID token" . to_string ( ) ) ,
752750 }
753751}
@@ -774,46 +772,42 @@ fn nonce_matches(id_token_nonce: &Nonce, state_nonce: &Nonce) -> Result<(), Stri
774772 Ok ( ( ) )
775773}
776774
777- #[ derive( Debug , Serialize , Deserialize ) ]
778- struct OidcLoginState {
779- /// The URL to redirect to after the login process is complete.
780- #[ serde( rename = "u" ) ]
781- initial_url : String ,
782- /// The CSRF token to use for the login process.
783- #[ serde( rename = "c" ) ]
784- csrf_token : CsrfToken ,
785- /// The source nonce to use for the login process. It must be checked against the hash
786- /// stored in the ID token.
787- #[ serde( rename = "n" ) ]
788- nonce : Nonce ,
789- }
790-
791- impl OidcLoginState {
792- fn new ( initial_url : String , auth_url : AuthUrlParams ) -> Self {
793- Self {
794- initial_url,
795- csrf_token : auth_url. csrf_token ,
796- nonce : auth_url. nonce ,
797- }
798- }
775+ fn create_nonce_cookie ( nonce : & Nonce ) -> Cookie < ' _ > {
776+ Cookie :: build ( SQLPAGE_NONCE_COOKIE_NAME , nonce. secret ( ) )
777+ . secure ( true )
778+ . http_only ( true )
779+ . same_site ( actix_web:: cookie:: SameSite :: Lax )
780+ . max_age ( AUTH_COOKIE_EXPIRATION )
781+ . path ( "/" )
782+ . finish ( )
799783}
800784
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 )
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 )
804788 . secure ( true )
805789 . http_only ( true )
806790 . same_site ( actix_web:: cookie:: SameSite :: Lax )
807791 . path ( "/" )
792+ . max_age ( actix_web:: cookie:: time:: Duration :: minutes ( 10 ) )
808793 . finish ( )
809794}
810795
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}" )
814- } ) ?;
815- serde_json:: from_str ( state_cookie. value ( ) )
816- . with_context ( || format ! ( "Failed to parse OIDC state from cookie: {state_cookie}" ) )
796+ fn get_nonce_from_cookie ( request : & ServiceRequest ) -> anyhow:: Result < Nonce > {
797+ let cookie = request
798+ . cookie ( SQLPAGE_NONCE_COOKIE_NAME )
799+ . with_context ( || format ! ( "No {SQLPAGE_NONCE_COOKIE_NAME} cookie found" ) ) ?;
800+ Ok ( Nonce :: new ( cookie. value ( ) . to_string ( ) ) )
801+ }
802+
803+ fn get_redirect_url_cookie (
804+ request : & ServiceRequest ,
805+ csrf_token : & CsrfToken ,
806+ ) -> anyhow:: Result < Cookie < ' static > > {
807+ let cookie_name = SQLPAGE_REDIRECT_URL_COOKIE_PREFIX . to_owned ( ) + csrf_token. secret ( ) ;
808+ request
809+ . cookie ( & cookie_name)
810+ . with_context ( || format ! ( "No {cookie_name} cookie found" ) )
817811}
818812
819813/// Given an audience, verify if it is trusted. The `client_id` is always trusted, independently of this function.
0 commit comments