Fail closed when persistent session signing material is unavailable

This commit is contained in:
archipelago
2026-10-06 01:30:04 -04:00
parent 5492080526
commit aa10bd1247
6 changed files with 316 additions and 50 deletions
+3 -3
View File
@@ -317,14 +317,14 @@ mod sanitize_tests {
/// Deterministic: same session token always produces the same CSRF token.
/// Survives backend restarts because it depends only on the session token
/// and the on-disk remember secret (not ephemeral state).
pub(crate) async fn derive_csrf_token(session_token: &str) -> String {
pub(crate) async fn derive_csrf_token(session_token: &str) -> std::io::Result<String> {
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let secret = SessionStore::load_or_create_remember_secret().await;
let secret = SessionStore::load_or_create_remember_secret().await?;
let mut mac = HmacSha256::new_from_slice(&secret).expect("HMAC key");
mac.update(format!("csrf:{}", session_token).as_bytes());
hex::encode(mac.finalize().into_bytes())
Ok(hex::encode(mac.finalize().into_bytes()))
}
/// Extract a named cookie value from headers.
+24 -13
View File
@@ -347,6 +347,16 @@ impl RpcHandler {
// Enforce authentication for non-allowlisted methods
let is_unauthenticated = UNAUTHENTICATED_METHODS.contains(&rpc_req.method.as_str());
if !is_unauthenticated || rpc_req.method.starts_with("auth.") {
if let Err(error) = SessionStore::load_or_create_remember_secret().await {
tracing::error!(%error, "Persistent session signing key unavailable");
return Ok(self.error_response(
503,
"Sign-in temporarily unavailable. Check server session storage.",
StatusCode::SERVICE_UNAVAILABLE,
));
}
}
let mut new_session_cookies: Option<(String, String)> = None;
if !is_unauthenticated {
let mut authenticated = match &session_token {
@@ -359,7 +369,7 @@ impl RpcHandler {
if let Some(remember) = extract_cookie(&parts.headers, "remember") {
if crate::session::SessionStore::validate_remember_token(&remember).await {
let new_token = self.session_store.create().await;
let new_csrf = derive_csrf_token(&new_token).await;
let new_csrf = derive_csrf_token(&new_token).await?;
tracing::info!("Auto-restored session from remember-me token");
new_session_cookies = Some((new_token, new_csrf));
authenticated = true;
@@ -407,7 +417,7 @@ impl RpcHandler {
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let secret = SessionStore::load_or_create_remember_secret().await;
let secret = SessionStore::load_or_create_remember_secret().await?;
let mut mac = match HmacSha256::new_from_slice(&secret) {
Ok(m) => m,
Err(_) => {
@@ -438,7 +448,7 @@ impl RpcHandler {
if let Some(token) = &session_token {
self.set_csrf_cookie(
&mut response,
&derive_csrf_token(token).await,
&derive_csrf_token(token).await?,
secure_suffix,
);
}
@@ -550,7 +560,7 @@ impl RpcHandler {
client_ip,
secure_suffix,
)
.await;
.await?;
Ok(response)
}
@@ -602,7 +612,7 @@ impl RpcHandler {
new_session_cookies: &Option<(String, String)>,
client_ip: std::net::IpAddr,
secure_suffix: &str,
) {
) -> Result<()> {
// Track failed login attempts for rate limiting
if method == "auth.login" && rpc_resp.error.is_some() {
self.login_rate_limiter.record_failure(client_ip).await;
@@ -642,7 +652,7 @@ impl RpcHandler {
if let Ok(Some(totp_data)) = self.auth_manager.get_totp_data().await {
if let Ok(secret) = crate::totp::decrypt_secret(&totp_data, password) {
let token = self.session_store.create_pending(secret).await;
let csrf_token = derive_csrf_token(&token).await;
let csrf_token = derive_csrf_token(&token).await?;
self.set_session_cookie(response, &token, secure_suffix);
self.set_csrf_cookie(response, &csrf_token, secure_suffix);
let totp_body = serde_json::json!({
@@ -655,8 +665,8 @@ impl RpcHandler {
}
} else {
let token = self.session_store.create().await;
let csrf_token = derive_csrf_token(&token).await;
let remember_token = self.session_store.create_remember_token().await;
let csrf_token = derive_csrf_token(&token).await?;
let remember_token = self.session_store.create_remember_token().await?;
self.set_session_cookie(response, &token, secure_suffix);
self.set_csrf_cookie(response, &csrf_token, secure_suffix);
self.set_remember_cookie(response, &remember_token, secure_suffix);
@@ -675,8 +685,8 @@ impl RpcHandler {
.map(|s| s.to_string());
if let Some(new_token) = new_token_opt {
let csrf_token = derive_csrf_token(&new_token).await;
let remember_token = self.session_store.create_remember_token().await;
let csrf_token = derive_csrf_token(&new_token).await?;
let remember_token = self.session_store.create_remember_token().await?;
self.set_session_cookie(response, &new_token, secure_suffix);
self.set_csrf_cookie(response, &csrf_token, secure_suffix);
self.set_remember_cookie(response, &remember_token, secure_suffix);
@@ -695,7 +705,7 @@ impl RpcHandler {
if method == "auth.changePassword" && rpc_resp.error.is_none() {
if let Some(token) = session_token {
let new_token = self.session_store.rotate(token).await;
let csrf_token = derive_csrf_token(&new_token).await;
let csrf_token = derive_csrf_token(&new_token).await?;
self.set_session_cookie(response, &new_token, secure_suffix);
self.set_csrf_cookie(response, &csrf_token, secure_suffix);
}
@@ -727,6 +737,7 @@ impl RpcHandler {
self.set_session_cookie(response, new_session, secure_suffix);
self.set_csrf_cookie(response, new_csrf, secure_suffix);
}
Ok(())
}
fn set_session_cookie(
@@ -891,7 +902,7 @@ mod csrf_recovery_tests {
.map(|v| v.to_str().unwrap())
.collect();
assert_eq!(cookies.len(), 1);
let expected = derive_csrf_token(&token).await;
let expected = derive_csrf_token(&token).await.unwrap();
assert_eq!(
cookies[0],
format!("csrf_token={expected}; SameSite=Lax; Path=/; Secure")
@@ -906,7 +917,7 @@ mod csrf_recovery_tests {
assert_eq!(stranger.status(), StatusCode::UNAUTHORIZED);
assert!(!stranger.headers().contains_key("set-cookie"));
assert!(!settings.exists());
let valid = derive_csrf_token(&token).await;
let valid = derive_csrf_token(&token).await.unwrap();
let response = handler
.handle(request(&token, Some(&valid), false))
.await