Private
Public Access
1
0

style: cargo fmt --all for v0.1.3 SSO changes
Some checks failed
CI Pipeline / Rust Format Check (push) Successful in 2s
CI Pipeline / Clippy Lints (push) Successful in 55s
CI Pipeline / Rust Unit Tests (push) Successful in 1m14s
CI Pipeline / Security Audit (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Failing after 11s
CI Pipeline / Build .deb & Release (push) Has been skipped

This commit is contained in:
2026-05-12 17:14:48 +00:00
parent 0fb804eefb
commit f04565ead7
2 changed files with 49 additions and 26 deletions

View File

@ -216,7 +216,12 @@ async fn azure_callback(
// Look up code_verifier from sso_sessions // Look up code_verifier from sso_sessions
let sso_session = match state.sso_sessions.remove(&state_token).map(|(_, v)| v) { let sso_session = match state.sso_sessions.remove(&state_token).map(|(_, v)| v) {
Some(s) => s, Some(s) => s,
None => return Err(error_redirect("bad_request", "Invalid or expired state token")), None => {
return Err(error_redirect(
"bad_request",
"Invalid or expired state token",
))
},
}; };
// Read Azure SSO config (including client_secret for token exchange) // Read Azure SSO config (including client_secret for token exchange)
@ -254,7 +259,7 @@ async fn azure_callback(
Err(e) => { Err(e) => {
tracing::error!(error = %e, "Failed to build HTTP client"); tracing::error!(error = %e, "Failed to build HTTP client");
return Err(error_redirect("internal_error", "HTTP client error")); return Err(error_redirect("internal_error", "HTTP client error"));
} },
}; };
let params = [ let params = [
@ -268,32 +273,36 @@ async fn azure_callback(
let form_params: Vec<(&str, String)> = params.to_vec(); let form_params: Vec<(&str, String)> = params.to_vec();
let token_resp = match client let token_resp = match client.post(&token_url).form(&form_params).send().await {
.post(&token_url)
.form(&form_params)
.send()
.await
{
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
tracing::error!(error = %e, "Token exchange request failed"); tracing::error!(error = %e, "Token exchange request failed");
return Err(error_redirect("sso_error", &format!("Token exchange failed: {}", e))); return Err(error_redirect(
} "sso_error",
&format!("Token exchange failed: {}", e),
));
},
}; };
if !token_resp.status().is_success() { if !token_resp.status().is_success() {
let status = token_resp.status(); let status = token_resp.status();
let body = token_resp.text().await.unwrap_or_default(); let body = token_resp.text().await.unwrap_or_default();
tracing::error!(status = %status, body = %body, "Token exchange failed"); tracing::error!(status = %status, body = %body, "Token exchange failed");
return Err(error_redirect("sso_error", &format!("Token exchange failed: HTTP {}", status))); return Err(error_redirect(
"sso_error",
&format!("Token exchange failed: HTTP {}", status),
));
} }
let token_data: TokenResponse = match token_resp.json().await { let token_data: TokenResponse = match token_resp.json().await {
Ok(d) => d, Ok(d) => d,
Err(e) => { Err(e) => {
tracing::error!(error = %e, "Failed to parse token response"); tracing::error!(error = %e, "Failed to parse token response");
return Err(error_redirect("internal_error", "Failed to parse token response")); return Err(error_redirect(
} "internal_error",
"Failed to parse token response",
));
},
}; };
// Verify id_token JWT signature using Azure AD JWKS and validate claims // Verify id_token JWT signature using Azure AD JWKS and validate claims
@ -306,8 +315,11 @@ async fn azure_callback(
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
tracing::error!(error = %e, "Failed to verify id_token"); tracing::error!(error = %e, "Failed to verify id_token");
return Err(error_redirect("internal_error", "Failed to verify id_token")); return Err(error_redirect(
} "internal_error",
"Failed to verify id_token",
));
},
}; };
let email = claims.email.unwrap_or_default(); let email = claims.email.unwrap_or_default();
@ -316,7 +328,10 @@ async fn azure_callback(
let preferred_username = claims.preferred_username.unwrap_or_else(|| email.clone()); let preferred_username = claims.preferred_username.unwrap_or_else(|| email.clone());
if email.is_empty() || oid.is_empty() { if email.is_empty() || oid.is_empty() {
return Err(error_redirect("sso_error", "Missing email or oid in id_token")); return Err(error_redirect(
"sso_error",
"Missing email or oid in id_token",
));
} }
// Look up or create user // Look up or create user
@ -332,7 +347,7 @@ async fn azure_callback(
Err(e) => { Err(e) => {
tracing::error!(error = %e, "Failed to look up SSO user"); tracing::error!(error = %e, "Failed to look up SSO user");
return Err(error_redirect("internal_error", "Database error")); return Err(error_redirect("internal_error", "Database error"));
} },
}; };
let user = match user_opt { let user = match user_opt {
@ -358,7 +373,7 @@ async fn azure_callback(
Err(e) => { Err(e) => {
tracing::error!(error = %e, "Failed to create SSO user"); tracing::error!(error = %e, "Failed to create SSO user");
return Err(error_redirect("internal_error", "Failed to create user")); return Err(error_redirect("internal_error", "Failed to create user"));
} },
}; };
log_event( log_event(
@ -411,15 +426,18 @@ async fn azure_callback(
Err(e) => { Err(e) => {
tracing::error!(error = %e, "Failed to issue access token"); tracing::error!(error = %e, "Failed to issue access token");
return Err(error_redirect("internal_error", "Token issuance failed")); return Err(error_redirect("internal_error", "Token issuance failed"));
} },
}; };
let raw_refresh = match refresh::issue(&state.db, user.id, None, None).await { let raw_refresh = match refresh::issue(&state.db, user.id, None, None).await {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
tracing::error!(error = %e, "Failed to issue refresh token"); tracing::error!(error = %e, "Failed to issue refresh token");
return Err(error_redirect("internal_error", "Refresh token issuance failed")); return Err(error_redirect(
} "internal_error",
"Refresh token issuance failed",
));
},
}; };
log_event( log_event(
@ -476,8 +494,7 @@ async fn verify_id_token(
jwks_cache: &Arc<Mutex<JwksCache>>, jwks_cache: &Arc<Mutex<JwksCache>>,
) -> Result<IdTokenClaims, String> { ) -> Result<IdTokenClaims, String> {
// 1. Decode JWT header to get the kid // 1. Decode JWT header to get the kid
let header = decode_header(token) let header = decode_header(token).map_err(|e| format!("Failed to decode JWT header: {}", e))?;
.map_err(|e| format!("Failed to decode JWT header: {}", e))?;
let kid = header.kid.ok_or("JWT header missing 'kid' field")?; let kid = header.kid.ok_or("JWT header missing 'kid' field")?;
@ -490,7 +507,7 @@ async fn verify_id_token(
(Some(_), Some(fetched)) => { (Some(_), Some(fetched)) => {
let elapsed = Utc::now().signed_duration_since(*fetched); let elapsed = Utc::now().signed_duration_since(*fetched);
elapsed.num_seconds() > JWKS_CACHE_TTL_SECS elapsed.num_seconds() > JWKS_CACHE_TTL_SECS
} },
}; };
if needs_fetch { if needs_fetch {

View File

@ -273,7 +273,10 @@ async fn get_settings(
let cfg = load_system_config(&state.db).await?; let cfg = load_system_config(&state.db).await?;
// Inject read-only config values from TOML file (not stored in DB) // Inject read-only config values from TOML file (not stored in DB)
let mut cfg = cfg; let mut cfg = cfg;
cfg.insert("sso_callback_url".to_string(), state.config.security.sso_callback_url.clone()); cfg.insert(
"sso_callback_url".to_string(),
state.config.security.sso_callback_url.clone(),
);
let azure = fetch_azure_sso_config(&state.db).await?; let azure = fetch_azure_sso_config(&state.db).await?;
Ok(Json(build_settings_response(&cfg, azure))) Ok(Json(build_settings_response(&cfg, azure)))
} }
@ -495,7 +498,10 @@ async fn update_settings(
let cfg = load_system_config(&state.db).await?; let cfg = load_system_config(&state.db).await?;
// Inject read-only config values from TOML file (not stored in DB) // Inject read-only config values from TOML file (not stored in DB)
let mut cfg = cfg; let mut cfg = cfg;
cfg.insert("sso_callback_url".to_string(), state.config.security.sso_callback_url.clone()); cfg.insert(
"sso_callback_url".to_string(),
state.config.security.sso_callback_url.clone(),
);
let azure = fetch_azure_sso_config(&state.db).await?; let azure = fetch_azure_sso_config(&state.db).await?;
Ok(Json(build_settings_response(&cfg, azure))) Ok(Json(build_settings_response(&cfg, azure)))
} }