@@ -151,11 +151,7 @@ fn get_app_host(config: &AppConfig) -> String {
151151 host
152152}
153153
154- fn build_absolute_uri (
155- app_host : & str ,
156- relative_path : & str ,
157- scheme : & str ,
158- ) -> anyhow:: Result < String > {
154+ fn build_absolute_uri ( app_host : & str , relative_path : & str , scheme : & str ) -> anyhow:: Result < String > {
159155 let mut base_url = Url :: parse ( & format ! ( "{scheme}://{app_host}" ) )
160156 . with_context ( || format ! ( "Failed to parse app_host: {app_host}" ) ) ?;
161157 base_url. set_path ( "" ) ;
@@ -442,13 +438,17 @@ struct LogoutParams {
442438
443439const LOGOUT_TOKEN_VALIDITY_SECONDS : i64 = 600 ;
444440
441+ fn parse_logout_params ( query : & str ) -> anyhow:: Result < LogoutParams > {
442+ Query :: < LogoutParams > :: from_query ( query)
443+ . with_context ( || format ! ( "{SQLPAGE_LOGOUT_URI}: missing required parameters" ) )
444+ . map ( |q| q. into_inner ( ) )
445+ }
446+
445447async fn process_oidc_logout (
446448 oidc_state : & OidcState ,
447449 request : & ServiceRequest ,
448450) -> anyhow:: Result < HttpResponse > {
449- let params = Query :: < LogoutParams > :: from_query ( request. query_string ( ) )
450- . with_context ( || format ! ( "{SQLPAGE_LOGOUT_URI}: missing required parameters" ) ) ?
451- . into_inner ( ) ;
451+ let params = parse_logout_params ( request. query_string ( ) ) ?;
452452
453453 verify_logout_params ( & params, & oidc_state. config . client_secret ) ?;
454454
@@ -463,11 +463,8 @@ async fn process_oidc_logout(
463463 let scheme = request. connection_info ( ) . scheme ( ) . to_string ( ) ;
464464 let mut response =
465465 if let Some ( end_session_endpoint) = oidc_state. get_end_session_endpoint ( ) . await {
466- let absolute_redirect_uri = build_absolute_uri (
467- & oidc_state. config . app_host ,
468- & logout_state. redirect_uri ,
469- & scheme,
470- ) ?;
466+ let absolute_redirect_uri =
467+ build_absolute_uri ( & oidc_state. config . app_host , & params. redirect_uri , & scheme) ?;
471468 let post_logout_redirect_uri =
472469 PostLogoutRedirectUrl :: new ( absolute_redirect_uri. clone ( ) ) . with_context ( || {
473470 format ! ( "Invalid post_logout_redirect_uri: {absolute_redirect_uri}" )
@@ -486,9 +483,9 @@ async fn process_oidc_logout(
486483 } else {
487484 log:: info!(
488485 "No end_session_endpoint, redirecting to {}" ,
489- logout_state . redirect_uri
486+ params . redirect_uri
490487 ) ;
491- build_redirect_response ( logout_state . redirect_uri )
488+ build_redirect_response ( params . redirect_uri )
492489 } ;
493490
494491 let mut auth_cookie = Cookie :: named ( SQLPAGE_AUTH_COOKIE_NAME ) ;
@@ -505,96 +502,66 @@ async fn process_oidc_logout(
505502 Ok ( response)
506503}
507504
508- #[ derive( Debug , Serialize , Deserialize ) ]
509- struct LogoutTokenPayload {
510- #[ serde( rename = "r" ) ]
511- redirect_uri : String ,
512- #[ serde( rename = "t" ) ]
513- timestamp : i64 ,
514- }
515-
516- fn create_logout_token ( redirect_uri : & str , client_secret : & str ) -> String {
505+ fn compute_logout_signature ( redirect_uri : & str , timestamp : i64 , client_secret : & str ) -> String {
517506 use base64:: Engine ;
518507 use hmac:: { Hmac , Mac } ;
519508 use sha2:: Sha256 ;
520509
521- let timestamp = chrono:: Utc :: now ( ) . timestamp ( ) ;
522- let payload = LogoutTokenPayload {
523- redirect_uri : redirect_uri. to_string ( ) ,
524- timestamp,
525- } ;
526- let payload_json = serde_json:: to_string ( & payload) . expect ( "payload is always serializable" ) ;
527- let payload_b64 =
528- base64:: engine:: general_purpose:: URL_SAFE_NO_PAD . encode ( payload_json. as_bytes ( ) ) ;
529-
530510 let mut mac = Hmac :: < Sha256 > :: new_from_slice ( client_secret. as_bytes ( ) )
531511 . expect ( "HMAC accepts any key size" ) ;
532- mac. update ( payload_b64. as_bytes ( ) ) ;
512+ mac. update ( redirect_uri. as_bytes ( ) ) ;
513+ mac. update ( & timestamp. to_be_bytes ( ) ) ;
533514 let signature = mac. finalize ( ) . into_bytes ( ) ;
534- let signature_b64 = base64:: engine:: general_purpose:: URL_SAFE_NO_PAD . encode ( signature) ;
535-
536- format ! ( "{payload_b64}.{signature_b64}" )
515+ base64:: engine:: general_purpose:: URL_SAFE_NO_PAD . encode ( signature)
537516}
538517
539- fn verify_and_decode_logout_token (
540- token : & str ,
541- client_secret : & str ,
542- ) -> anyhow:: Result < LogoutTokenPayload > {
518+ fn verify_logout_params ( params : & LogoutParams , client_secret : & str ) -> anyhow:: Result < ( ) > {
543519 use base64:: Engine ;
544- use hmac:: { Hmac , Mac } ;
545- use sha2:: Sha256 ;
546520
547- let parts: Vec < & str > = token. split ( '.' ) . collect ( ) ;
548- if parts. len ( ) != 2 {
549- anyhow:: bail!( "Invalid logout token format" ) ;
550- }
521+ let expected_signature =
522+ compute_logout_signature ( & params. redirect_uri , params. timestamp , client_secret) ;
551523
552- let payload_b64 = parts[ 0 ] ;
553- let signature_b64 = parts[ 1 ] ;
554-
555- let mut mac = Hmac :: < Sha256 > :: new_from_slice ( client_secret. as_bytes ( ) )
556- . expect ( "HMAC accepts any key size" ) ;
557- mac. update ( payload_b64. as_bytes ( ) ) ;
558-
559- let expected_signature = mac. finalize ( ) . into_bytes ( ) ;
560524 let provided_signature = base64:: engine:: general_purpose:: URL_SAFE_NO_PAD
561- . decode ( signature_b64 )
562- . with_context ( || "Invalid logout token signature encoding" ) ?;
525+ . decode ( & params . signature )
526+ . with_context ( || "Invalid logout signature encoding" ) ?;
563527
564- if expected_signature[ ..] != provided_signature[ ..] {
565- anyhow:: bail!( "Invalid logout token signature" ) ;
566- }
567-
568- let payload_json = base64:: engine:: general_purpose:: URL_SAFE_NO_PAD
569- . decode ( payload_b64)
570- . with_context ( || "Invalid logout token payload encoding" ) ?;
528+ let expected_signature_bytes = base64:: engine:: general_purpose:: URL_SAFE_NO_PAD
529+ . decode ( & expected_signature)
530+ . with_context ( || "Failed to decode expected signature" ) ?;
571531
572- let payload: LogoutTokenPayload =
573- serde_json:: from_slice ( & payload_json) . with_context ( || "Invalid logout token payload" ) ?;
532+ if expected_signature_bytes[ ..] != provided_signature[ ..] {
533+ anyhow:: bail!( "Invalid logout signature" ) ;
534+ }
574535
575536 let now = chrono:: Utc :: now ( ) . timestamp ( ) ;
576- if now - payload . timestamp > LOGOUT_TOKEN_VALIDITY_SECONDS {
537+ if now - params . timestamp > LOGOUT_TOKEN_VALIDITY_SECONDS {
577538 anyhow:: bail!( "Logout token has expired" ) ;
578539 }
579- if payload . timestamp > now + 60 {
540+ if params . timestamp > now + 60 {
580541 anyhow:: bail!( "Logout token timestamp is in the future" ) ;
581542 }
582543
583- if !payload . redirect_uri . starts_with ( '/' ) || payload . redirect_uri . starts_with ( "//" ) {
584- anyhow:: bail!( "Invalid redirect URI in logout token " ) ;
544+ if !params . redirect_uri . starts_with ( '/' ) || params . redirect_uri . starts_with ( "//" ) {
545+ anyhow:: bail!( "Invalid redirect URI" ) ;
585546 }
586547
587- Ok ( payload )
548+ Ok ( ( ) )
588549}
589550
590551#[ must_use]
591552pub fn create_logout_url ( redirect_uri : & str , site_prefix : & str , client_secret : & str ) -> String {
592- let token = create_logout_token ( redirect_uri, client_secret) ;
553+ let timestamp = chrono:: Utc :: now ( ) . timestamp ( ) ;
554+ let signature = compute_logout_signature ( redirect_uri, timestamp, client_secret) ;
593555 format ! (
594- "{}{}?token ={}" ,
556+ "{}{}?redirect_uri={}×tamp={}&signature ={}" ,
595557 site_prefix. trim_end_matches( '/' ) ,
596558 SQLPAGE_LOGOUT_URI ,
597- percent_encoding:: percent_encode( token. as_bytes( ) , percent_encoding:: NON_ALPHANUMERIC )
559+ percent_encoding:: percent_encode(
560+ redirect_uri. as_bytes( ) ,
561+ percent_encoding:: NON_ALPHANUMERIC
562+ ) ,
563+ timestamp,
564+ percent_encoding:: percent_encode( signature. as_bytes( ) , percent_encoding:: NON_ALPHANUMERIC )
598565 )
599566}
600567
@@ -1054,6 +1021,7 @@ fn validate_redirect_url(url: String) -> String {
10541021mod tests {
10551022 use super :: * ;
10561023 use actix_web:: http:: StatusCode ;
1024+ use openidconnect:: url:: Url ;
10571025
10581026 #[ test]
10591027 fn login_redirects_use_see_other ( ) {
@@ -1082,4 +1050,17 @@ mod tests {
10821050 . expect ( "Auth0 returns updated_at as RFC3339 string, not unix timestamp" ) ;
10831051 assert ! ( claims. updated_at( ) . is_some( ) ) ;
10841052 }
1053+
1054+ #[ test]
1055+ fn logout_url_generation_and_parsing_are_compatible ( ) {
1056+ let secret = "super_secret_key" ;
1057+ let generated = create_logout_url ( "/after" , "https://example.com" , secret) ;
1058+
1059+ let parsed = Url :: parse ( & generated) . expect ( "generated URL should be valid" ) ;
1060+ assert_eq ! ( parsed. path( ) , SQLPAGE_LOGOUT_URI ) ;
1061+
1062+ let params = parse_logout_params ( parsed. query ( ) . expect ( "query string is present" ) )
1063+ . expect ( "generated URL should parse" ) ;
1064+ verify_logout_params ( & params, secret) . expect ( "generated URL should validate" ) ;
1065+ }
10851066}
0 commit comments