use crate::cookies::MAX_COOKIE_VALUE_BYTES;
use crate::session::config::SessionConfig;
use crate::session::data::SessionData;
use crate::session::id::SessionId;
use crate::session::layer::handle::SessionHandle;
use crate::session::layer::signing::SigningKeyRing;
use crate::session::store::SessionStore;
use axess_rng::SystemRng;
use subtle::ConstantTimeEq;
use tower_cookies::cookie::Cookie;
pub(crate) struct LoadOutcome {
pub(crate) id: SessionId,
pub(crate) data: SessionData,
pub(crate) existing_id: Option<SessionId>,
pub(crate) binding_invalidated: bool,
pub(crate) rotation_fallback_used: bool,
}
pub(crate) struct FinalizeOutcome {
pub(crate) final_id: SessionId,
pub(crate) session_changed: bool,
}
pub(crate) async fn load_session<S>(
store: &S,
signing_keys: &SigningKeyRing,
config: &SessionConfig,
metrics: Option<&dyn crate::metrics::AuthnMetrics>,
headers: &axum::http::HeaderMap,
current_fingerprint: Option<&str>,
previous_fingerprint: Option<&str>,
) -> LoadOutcome
where
S: SessionStore + Send + Sync + 'static,
S::Error: std::fmt::Display + Send + Sync + 'static,
{
let cookie_value = crate::cookies::extract_named_cookie(
headers,
config.cookie_name.as_ref(),
MAX_COOKIE_VALUE_BYTES,
);
let verified = cookie_value
.as_deref()
.and_then(|v| signing_keys.decode_cookie(v));
let mut rotation_fallback_used = verified
.as_ref()
.map(|v| v.verified_by_previous)
.unwrap_or(false);
let (mut existing_id, mut session_data) = if let Some(verified) = verified {
match store.load(&verified.id).await {
Ok(Some(mut data)) => {
if data.migrate() {
tracing::debug!(
new_version = data.version,
"session data migrated to newer schema version"
);
}
(Some(verified.id), data)
}
Ok(None) => (None, SessionData::default()),
Err(e) => {
tracing::warn!(
error = %e,
"session store load failed; falling back to empty session"
);
(None, SessionData::default())
}
}
} else {
(None, SessionData::default())
};
let mut binding_invalidated = false;
if let (Some(stored_hash), Some(current_hash)) =
(session_data.fingerprint.clone(), current_fingerprint)
{
if bool::from(stored_hash.as_bytes().ct_eq(current_hash.as_bytes())) {
} else if let Some(previous_hash) = previous_fingerprint
&& bool::from(stored_hash.as_bytes().ct_eq(previous_hash.as_bytes()))
{
tracing::debug!(
"session fingerprint verified with previous (rotated) signing key; \
re-storing under current key"
);
session_data.fingerprint = Some(current_hash.to_string());
rotation_fallback_used = true;
} else {
tracing::warn!(
"session fingerprint mismatch; invalidating session (possible hijacking)"
);
if let Some(m) = metrics {
m.session_binding_mismatch();
}
session_data = SessionData::default();
binding_invalidated = true;
existing_id = None;
}
}
let rng = SystemRng;
let id = existing_id.unwrap_or_else(|| SessionId::new(&rng));
LoadOutcome {
id,
data: session_data,
existing_id,
binding_invalidated,
rotation_fallback_used,
}
}
pub(crate) async fn finalize_session<S>(
store: &S,
config: &SessionConfig,
metrics: Option<&dyn crate::metrics::AuthnMetrics>,
handle: &SessionHandle,
existing_id: Option<SessionId>,
) -> FinalizeOutcome
where
S: SessionStore + Send + Sync + 'static,
S::Error: std::fmt::Display + Send + Sync + 'static,
{
let mut guard = handle.0.write().await;
if config.max_custom_bytes > 0 && guard.modified {
let custom_size = serde_json::to_vec(&guard.data.custom)
.map(|v| v.len())
.unwrap_or(0);
if custom_size > config.max_custom_bytes {
tracing::warn!(
custom_size,
max = config.max_custom_bytes,
"session custom data exceeds size limit; clearing custom data"
);
guard.data.custom = serde_json::Value::default();
}
}
let session_changed = guard.modified || guard.regenerate || existing_id.is_none();
if session_changed
&& (guard.regenerate || existing_id.is_none())
&& let Some(m) = metrics
{
m.session_created();
}
let final_id = if session_changed {
if guard.regenerate {
let rng = SystemRng;
let old_id = guard.pre_cycle_id.take().unwrap_or_else(|| {
let prev = guard.id;
guard.id = SessionId::new(&rng);
prev
});
let new_id = guard.id;
match store.cycle(&old_id, &new_id, &guard.data, config.ttl).await {
Ok(()) => new_id,
Err(e) => {
tracing::error!(
error = %e,
"session store cycle failed; clearing session (fail closed)"
);
guard.data = SessionData::default();
guard.id = old_id;
old_id
}
}
} else {
if let Err(e) = store.save(&guard.id, &guard.data, config.ttl).await {
tracing::warn!(
error = %e,
"session store save failed; session changes may be lost"
);
}
guard.id
}
} else {
guard.id
};
FinalizeOutcome {
final_id,
session_changed,
}
}
pub(crate) fn build_set_cookie(
signing_keys: &SigningKeyRing,
config: &SessionConfig,
id: SessionId,
) -> Option<axum::http::HeaderValue> {
let cookie_value = signing_keys.sign_cookie(id);
let mut cookie = Cookie::new(config.cookie_name.as_ref().to_string(), cookie_value);
cookie.set_http_only(config.http_only);
cookie.set_secure(config.secure);
cookie.set_same_site(config.same_site);
cookie.set_path(config.path.as_ref().to_string());
cookie.set_max_age(tower_cookies::cookie::time::Duration::seconds(
config.ttl.as_secs().min(i64::MAX as u64) as i64,
));
axum::http::HeaderValue::from_str(&cookie.to_string()).ok()
}
#[cfg(test)]
mod helper_tests;