diff --git a/examples/official-site/sqlpage/migrations/61_oidc_functions.sql b/examples/official-site/sqlpage/migrations/61_oidc_functions.sql index 71d1d849..44c3bc32 100644 --- a/examples/official-site/sqlpage/migrations/61_oidc_functions.sql +++ b/examples/official-site/sqlpage/migrations/61_oidc_functions.sql @@ -192,6 +192,8 @@ When a user visits this URL, SQLPage will: 2. Redirect the user to the OIDC provider''s logout endpoint (if available) 3. Finally redirect back to the specified `redirect_uri` +The generated link now appends the current user''s OIDC `sub` identifier as the `user_id` query parameter, so you can display or log which account will be signed out. If no user is currently authenticated, the function simply returns the provided `redirect_uri`, avoiding unnecessary redirects. + ## Security Features This function provides protection against **Cross-Site Request Forgery (CSRF)** attacks: diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index de15467b..02a88293 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -876,10 +876,15 @@ async fn oidc_logout_url<'a>( ); } + let Some(claims) = request.oidc_claims.as_ref() else { + return Ok(Some(redirect_uri.to_string())); + }; + let logout_url = crate::webserver::oidc::create_logout_url( redirect_uri, &request.app_state.config.site_prefix, &oidc_state.config.client_secret, + Some(claims.subject().as_str()), ); Ok(Some(logout_url)) diff --git a/src/webserver/oidc.rs b/src/webserver/oidc.rs index ac16126b..1fe06118 100644 --- a/src/webserver/oidc.rs +++ b/src/webserver/oidc.rs @@ -551,14 +551,22 @@ fn verify_logout_params(params: &LogoutParams, client_secret: &str) -> anyhow::R } #[must_use] -pub fn create_logout_url(redirect_uri: &str, site_prefix: &str, client_secret: &str) -> String { +pub fn create_logout_url( + redirect_uri: &str, + site_prefix: &str, + client_secret: &str, + user_id: Option<&str>, +) -> String { let timestamp = chrono::Utc::now().timestamp(); let signature = compute_logout_signature(redirect_uri, timestamp, client_secret); - let query = form_urlencoded::Serializer::new(String::new()) - .append_pair("redirect_uri", redirect_uri) - .append_pair("timestamp", ×tamp.to_string()) - .append_pair("signature", &signature) - .finish(); + let mut serializer = form_urlencoded::Serializer::new(String::new()); + serializer.append_pair("redirect_uri", redirect_uri); + serializer.append_pair("timestamp", ×tamp.to_string()); + serializer.append_pair("signature", &signature); + if let Some(user_id) = user_id { + serializer.append_pair("user_id", user_id); + } + let query = serializer.finish(); format!( "{}{}?{}", site_prefix.trim_end_matches('/'), @@ -1056,10 +1064,18 @@ mod tests { #[test] fn logout_url_generation_and_parsing_are_compatible() { let secret = "super_secret_key"; - let generated = create_logout_url("/after", "https://example.com", secret); + let user_id = "user-123"; + let generated = create_logout_url("/after", "https://example.com", secret, Some(user_id)); let parsed = Url::parse(&generated).expect("generated URL should be valid"); assert_eq!(parsed.path(), SQLPAGE_LOGOUT_URI); + let mut pairs = parsed.query_pairs(); + assert_eq!( + pairs + .find(|(key, _)| key == "user_id") + .map(|(_, value)| value.to_string()), + Some(user_id.to_string()) + ); let params = parse_logout_params(parsed.query().expect("query string is present")) .expect("generated URL should parse");