Skip to content

Commit 485f3f9

Browse files
committed
refactor: Simplify OIDC logout processing and enhance logout token handling
This commit refactors the OIDC logout process by introducing a new function, `parse_logout_params`, to streamline the extraction of logout parameters from the request. It also updates the logout token creation and verification logic, improving security by ensuring the signature is computed correctly. Additionally, the `create_logout_url` function is modified to include a timestamp and signature in the generated URL, enhancing the logout flow's integrity.
1 parent 1b0912c commit 485f3f9

File tree

1 file changed

+55
-74
lines changed

1 file changed

+55
-74
lines changed

src/webserver/oidc.rs

Lines changed: 55 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -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

443439
const 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+
445447
async 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]
591552
pub 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={}&timestamp={}&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 {
10541021
mod 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

Comments
 (0)