codlet_core/auth/
session.rs1use crate::audit::{AuditSink, CodeAuthEvent};
9use crate::clock::Clock;
10use crate::cookie::CookiePolicy;
11use crate::hashing::{KeyProvider, SecretDomain, SecretHasher};
12use crate::rng::RandomSource;
13use crate::secret::{SessionId, SessionSecret};
14use crate::state::{SessionValidationOutcome, classify_session};
15use crate::store::code::expires_at_from_ttl;
16use crate::store::session::{SessionRecord, SessionStore};
17
18use super::error::{IssuedSession, RedeemSuccess, SessionError};
19
20pub struct SessionManager<SS, K, C, A> {
27 store: SS,
28 hasher: SecretHasher<K>,
29 clock: C,
30 audit: A,
31 cookie_policy: CookiePolicy,
32}
33
34impl<SS, K, C, A> SessionManager<SS, K, C, A>
35where
36 SS: SessionStore,
37 K: KeyProvider,
38 C: Clock,
39 A: AuditSink,
40{
41 #[must_use]
43 pub fn new(
44 store: SS,
45 hasher: SecretHasher<K>,
46 clock: C,
47 audit: A,
48 cookie_policy: CookiePolicy,
49 ) -> Self {
50 Self {
51 store,
52 hasher,
53 clock,
54 audit,
55 cookie_policy,
56 }
57 }
58
59 pub async fn issue<R: RandomSource>(
72 &self,
73 success: &RedeemSuccess,
74 session_id: SessionId,
75 rng: &mut R,
76 ) -> Result<IssuedSession, SessionError> {
77 let mut raw = [0u8; 32];
79 rng.fill_bytes(&mut raw)
80 .map_err(|e| SessionError::Internal {
81 cause: format!("rng: {e}"),
82 public: crate::error::PublicSessionError::TemporarilyUnavailable,
83 })?;
84
85 let secret_hex = hex_lower(&raw);
87 let secret = SessionSecret::new(secret_hex.clone());
88
89 let (lookup_key, key_version) = self
90 .hasher
91 .lookup_key(SecretDomain::Session, secret.expose())
92 .map_err(SessionError::from_key)?;
93
94 let now = self.clock.unix_now();
95 let expires_at = expires_at_from_ttl(now, self.cookie_policy.max_age_duration());
96
97 self.store
98 .insert_session(SessionRecord {
99 id: session_id.clone(),
100 lookup_key,
101 key_version,
102 subject: success.subject.clone(),
103 created_at: now,
104 expires_at,
105 })
106 .await
107 .map_err(SessionError::from_store)?;
108
109 self.audit.record(CodeAuthEvent::SessionIssued {
110 session_id: session_id.clone(),
111 subject_id: success.subject.clone(),
112 });
113
114 let set_cookie = self.cookie_policy.build_set_cookie(secret.expose());
115 Ok(IssuedSession {
116 session_id,
117 set_cookie,
118 })
119 }
120
121 pub async fn validate(
132 &self,
133 cookie_value: &str,
134 ) -> Result<SessionValidationOutcome, SessionError> {
135 let (lookup_key, _) = self
136 .hasher
137 .lookup_key(SecretDomain::Session, cookie_value)
138 .map_err(SessionError::from_key)?;
139
140 let now = self.clock.unix_now();
141 let record = self
142 .store
143 .find_active_session(&[lookup_key], now)
144 .await
145 .map_err(SessionError::from_store)?;
146
147 let outcome = classify_session(record.map(|r| (r.subject, r.id, r.expires_at)));
148
149 if !outcome.is_authenticated() {
150 self.audit.record(CodeAuthEvent::SessionValidateFailed);
151 }
152
153 Ok(outcome)
154 }
155
156 pub async fn revoke(&self, session_id: &SessionId) -> Result<String, SessionError> {
164 let now = self.clock.unix_now();
165 self.store
166 .revoke_session(session_id, now)
167 .await
168 .map_err(SessionError::from_store)?;
169
170 self.audit.record(CodeAuthEvent::SessionRevoked {
171 session_id: session_id.clone(),
172 });
173
174 Ok(self.cookie_policy.build_clear_cookie())
175 }
176
177 #[must_use]
180 pub fn cookie_policy(&self) -> &CookiePolicy {
181 &self.cookie_policy
182 }
183}
184
185fn hex_lower(bytes: &[u8]) -> String {
186 const HEX: &[u8; 16] = b"0123456789abcdef";
187 let mut s = String::with_capacity(bytes.len() * 2);
188 for &b in bytes {
189 s.push(HEX[(b >> 4) as usize] as char);
190 s.push(HEX[(b & 0xf) as usize] as char);
191 }
192 s
193}