mod handle;
mod lifecycle;
mod service;
mod signing;
pub(crate) use handle::SessionHandle;
#[cfg(any(test, feature = "testing"))]
pub(crate) use handle::SessionInner;
pub use service::SessionService;
#[cfg(feature = "device")]
use crate::device::resolver::{DeviceResolver, ErasedDeviceResolver};
use crate::session::binding::SessionBinding;
use crate::session::config::SessionConfig;
use crate::session::store::SessionStore;
use signing::{SigningKeys, hkdf_expand_subkey};
use std::{sync::Arc, time::Duration};
use tower_cookies::cookie::SameSite;
#[derive(Clone)]
pub struct SessionLayer<S> {
pub(super) store: S,
pub(super) signing_keys: Arc<SigningKeys>,
pub(super) config: Arc<SessionConfig>,
pub(super) binding: Option<Arc<dyn SessionBinding>>,
pub(super) metrics: Option<Arc<dyn crate::metrics::AuthnMetrics>>,
#[cfg(feature = "device")]
pub(super) device_resolver: Option<Arc<dyn ErasedDeviceResolver>>,
}
impl<S: SessionStore> SessionLayer<S> {
pub fn new(store: S, signing_key: [u8; 32]) -> Self {
Self {
store,
signing_keys: Arc::new(SigningKeys::from_master(signing_key)),
config: Arc::new(SessionConfig::default()),
binding: None,
metrics: None,
#[cfg(feature = "device")]
device_resolver: None,
}
}
fn config_mut(&mut self) -> &mut SessionConfig {
debug_assert!(
Arc::strong_count(&self.config) == 1,
"SessionLayer setter called after the layer was cloned (strong_count = {}). \
The mutation will deep-copy SessionConfig and only affect this clone. Configure \
the layer fully before passing it to `tower::Layer` / `Router::layer`.",
Arc::strong_count(&self.config),
);
Arc::make_mut(&mut self.config)
}
pub fn derive_subkey(&self, info: &'static [u8]) -> zeroize::Zeroizing<[u8; 32]> {
zeroize::Zeroizing::new(hkdf_expand_subkey(&self.signing_keys.master, info))
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.config_mut().ttl = ttl;
self
}
pub fn with_cookie_name(mut self, name: impl Into<Arc<str>>) -> Self {
self.config_mut().cookie_name = name.into();
self
}
pub fn with_secure(mut self, secure: bool) -> Self {
if !secure {
tracing::warn!(
"SessionLayer: Secure cookie flag disabled; session cookies \
will be sent over plain HTTP. Do not use in production."
);
}
self.config_mut().secure = secure;
self
}
pub fn with_same_site(mut self, same_site: SameSite) -> Self {
self.config_mut().same_site = same_site;
self
}
pub fn with_http_only(mut self, http_only: bool) -> Self {
self.config_mut().http_only = http_only;
self
}
pub fn with_max_custom_bytes(mut self, max: usize) -> Self {
self.config_mut().max_custom_bytes = max;
self
}
pub fn with_path(mut self, path: impl Into<Arc<str>>) -> Self {
self.config_mut().path = path.into();
self
}
pub fn with_binding(mut self, binding: impl SessionBinding) -> Self {
self.binding = Some(Arc::new(binding));
self
}
pub fn with_metrics(mut self, metrics: impl crate::metrics::AuthnMetrics) -> Self {
self.metrics = Some(Arc::new(metrics));
self
}
#[cfg(feature = "device")]
pub fn with_device_resolver<R>(mut self, resolver: R) -> Self
where
R: DeviceResolver,
{
self.device_resolver = Some(Arc::new(resolver));
self
}
}
#[cfg(all(test, debug_assertions))]
mod make_mut_tests {
use super::*;
use crate::session::store::MemorySessionStore;
#[test]
#[should_panic(expected = "SessionLayer setter called after the layer was cloned")]
fn setter_after_clone_panics_in_debug() {
let store = MemorySessionStore::new();
let layer = SessionLayer::new(store, [0u8; 32]);
let cloned = layer.clone();
layer.with_ttl(Duration::from_secs(60));
drop(cloned);
}
#[test]
fn setter_before_clone_does_not_panic() {
let store = MemorySessionStore::new();
let layer = SessionLayer::new(store, [0u8; 32])
.with_ttl(Duration::from_secs(60))
.with_secure(false);
drop(layer.clone());
}
}
#[cfg(test)]
mod with_secure_warning_tests {
use super::*;
use crate::session::store::MemorySessionStore;
use crate::testing::mock_tracing::TracingCapture;
#[test]
fn with_secure_false_emits_warning() {
let capture = TracingCapture::install();
let store = MemorySessionStore::new();
drop(SessionLayer::new(store, [0u8; 32]).with_secure(false));
assert!(
capture.contains_at_level(tracing::Level::WARN, "Secure cookie flag disabled"),
"with_secure(false) must warn about plain-HTTP cookies"
);
}
#[test]
fn with_secure_true_does_not_emit_warning() {
let capture = TracingCapture::install();
let store = MemorySessionStore::new();
drop(SessionLayer::new(store, [0u8; 32]).with_secure(true));
assert!(
!capture.contains_at_level(tracing::Level::WARN, "Secure cookie flag disabled"),
"with_secure(true) must NOT warn; `delete !` mutation would invert this"
);
}
}