use crate::audit::{AuditSink, CodeAuthEvent};
use crate::clock::Clock;
use crate::cookie::CookiePolicy;
use crate::hashing::{KeyProvider, SecretDomain, SecretHasher};
use crate::rng::RandomSource;
use crate::secret::{SessionId, SessionSecret};
use crate::state::{SessionValidationOutcome, classify_session};
use crate::store::code::expires_at_from_ttl;
use crate::store::session::{SessionRecord, SessionStore};
use super::error::{IssuedSession, RedeemSuccess, SessionError};
pub struct SessionManager<SS, K, C, A> {
store: SS,
hasher: SecretHasher<K>,
clock: C,
audit: A,
cookie_policy: CookiePolicy,
}
impl<SS, K, C, A> SessionManager<SS, K, C, A>
where
SS: SessionStore,
K: KeyProvider,
C: Clock,
A: AuditSink,
{
#[must_use]
pub fn new(
store: SS,
hasher: SecretHasher<K>,
clock: C,
audit: A,
cookie_policy: CookiePolicy,
) -> Self {
Self {
store,
hasher,
clock,
audit,
cookie_policy,
}
}
pub async fn issue<R: RandomSource>(
&self,
success: &RedeemSuccess,
session_id: SessionId,
rng: &mut R,
) -> Result<IssuedSession, SessionError> {
let mut raw = [0u8; 32];
rng.fill_bytes(&mut raw)
.map_err(|e| SessionError::Internal {
cause: format!("rng: {e}"),
public: crate::error::PublicSessionError::TemporarilyUnavailable,
})?;
let secret_hex = hex_lower(&raw);
let secret = SessionSecret::new(secret_hex.clone());
let (lookup_key, key_version) = self
.hasher
.lookup_key(SecretDomain::Session, secret.expose())
.map_err(SessionError::from_key)?;
let now = self.clock.unix_now();
let expires_at = expires_at_from_ttl(now, self.cookie_policy.max_age_duration());
self.store
.insert_session(SessionRecord {
id: session_id.clone(),
lookup_key,
key_version,
subject: success.subject.clone(),
created_at: now,
expires_at,
})
.await
.map_err(SessionError::from_store)?;
self.audit.record(CodeAuthEvent::SessionIssued {
session_id: session_id.clone(),
subject_id: success.subject.clone(),
});
let set_cookie = self.cookie_policy.build_set_cookie(secret.expose());
Ok(IssuedSession {
session_id,
set_cookie,
})
}
pub async fn validate(
&self,
cookie_value: &str,
) -> Result<SessionValidationOutcome, SessionError> {
let (lookup_key, _) = self
.hasher
.lookup_key(SecretDomain::Session, cookie_value)
.map_err(SessionError::from_key)?;
let now = self.clock.unix_now();
let record = self
.store
.find_active_session(&[lookup_key], now)
.await
.map_err(SessionError::from_store)?;
let outcome = classify_session(record.map(|r| (r.subject, r.id, r.expires_at)));
if !outcome.is_authenticated() {
self.audit.record(CodeAuthEvent::SessionValidateFailed);
}
Ok(outcome)
}
pub async fn revoke(&self, session_id: &SessionId) -> Result<String, SessionError> {
let now = self.clock.unix_now();
self.store
.revoke_session(session_id, now)
.await
.map_err(SessionError::from_store)?;
self.audit.record(CodeAuthEvent::SessionRevoked {
session_id: session_id.clone(),
});
Ok(self.cookie_policy.build_clear_cookie())
}
#[must_use]
pub fn cookie_policy(&self) -> &CookiePolicy {
&self.cookie_policy
}
}
fn hex_lower(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(bytes.len() * 2);
for &b in bytes {
s.push(HEX[(b >> 4) as usize] as char);
s.push(HEX[(b & 0xf) as usize] as char);
}
s
}