1use super::flash::{FlashMessage, deserialize_flash_messages, serialize_flash_messages};
2use super::session::{
3 BrowserInstant, BrowserSessionRecord, BrowserSessionStatus, DistributedSessionStoreClient,
4 IssuedBrowserSession, RotatedBrowserSession, SessionIssueRequest, SessionStoreBackend,
5 SessionStoreBackendKind, issue_session,
6};
7use super::support::{
8 FLASH_COOKIE_MAX_AGE_SECS, map_flash_cookie_error, map_session_cookie_error,
9 validate_browser_value,
10};
11use super::*;
12use std::time::Duration;
13use thiserror::Error;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ResolvedBrowserRequest {
17 pub session: SessionContext,
18 pub principal_id: Option<String>,
19 pub flash_messages: Vec<FlashMessage>,
20 pub response_cookies: Vec<String>,
21}
22
23#[derive(Debug, Error, PartialEq, Eq)]
24pub enum RuntimeBrowserError {
25 #[error("browser value `{field}` must not be empty")]
26 EmptyValue { field: &'static str },
27 #[error("session cookie failed validation: {reason}")]
28 InvalidSessionCookie { reason: String },
29 #[error("flash cookie failed validation: {reason}")]
30 InvalidFlashCookie { reason: String },
31 #[error("session `{session_id}` is not present in the server-side store")]
32 UnknownSession { session_id: String },
33 #[error("session `{session_id}` has expired")]
34 ExpiredSession { session_id: String },
35 #[error("session `{session_id}` has been revoked")]
36 RevokedSession { session_id: String },
37 #[error("flash cookie payload is malformed")]
38 InvalidFlashPayload,
39 #[error("flash cookie contains unknown level `{level}`")]
40 InvalidFlashLevel { level: String },
41 #[error(
42 "live browser session store `{kind:?}` for `{scope}` requires an explicit distributed runtime"
43 )]
44 LiveSharedSessionStoreUnavailable {
45 kind: SessionStoreBackendKind,
46 scope: String,
47 },
48 #[error("live browser session store `{kind:?}` for `{scope}` failed: {reason}")]
49 LiveSharedSessionStoreFailure {
50 kind: SessionStoreBackendKind,
51 scope: String,
52 reason: String,
53 },
54}
55
56#[derive(Debug, Error, Clone, PartialEq, Eq)]
57pub enum BrowserHostBuildError {
58 #[error("memory session stores are test-only and cannot back a live browser host")]
59 MemoryStoreRequiresTestOnlyBrowserHost,
60 #[error("memory session stores cannot use a distributed session client")]
61 MemoryStoreCannotUseDistributedClient,
62 #[error(
63 "live browser session stores require an explicit distributed runtime; `{kind:?}` is not live-supported"
64 )]
65 LiveSharedSessionStoreRequiresExplicitRuntime { kind: SessionStoreBackendKind },
66 #[error(
67 "live browser session store `{kind:?}` for `{scope}` could not be initialized at `{path}`: {reason}"
68 )]
69 LiveSharedSessionStoreInitializationFailed {
70 kind: SessionStoreBackendKind,
71 scope: String,
72 path: String,
73 reason: String,
74 },
75 #[error("session store client kind mismatch: expected `{expected:?}`, got `{actual:?}`")]
76 SessionStoreClientKindMismatch {
77 expected: SessionStoreBackendKind,
78 actual: SessionStoreBackendKind,
79 },
80}
81
82#[derive(Debug, Clone)]
83pub struct BrowserHost {
84 pub customer_app: String,
85 pub services: BrowserSecurityServices,
86 pub(super) session_store_kind: SessionStoreBackendKind,
87 pub(super) sessions: SessionStoreBackend,
88}
89
90impl BrowserHost {
91 #[cfg(test)]
92 pub(crate) fn new_with_scope(
93 customer_app: String,
94 services: BrowserSecurityServices,
95 backend_scope: impl Into<String>,
96 ) -> Result<Self, BrowserHostBuildError> {
97 let backend_scope = backend_scope.into();
98 let (session_store_kind, sessions) =
99 SessionStoreBackend::shared(&customer_app, &services.sessions, &backend_scope)?;
100 Ok(Self {
101 customer_app,
102 services,
103 session_store_kind,
104 sessions,
105 })
106 }
107
108 pub fn with_session_store_client(
109 customer_app: String,
110 services: BrowserSecurityServices,
111 client: DistributedSessionStoreClient,
112 ) -> Result<Self, BrowserHostBuildError> {
113 let (session_store_kind, sessions) =
114 SessionStoreBackend::with_client(&services.sessions, client)?;
115 if !sessions.is_live_shared_state_supported() {
116 return Err(
117 BrowserHostBuildError::LiveSharedSessionStoreRequiresExplicitRuntime {
118 kind: session_store_kind,
119 },
120 );
121 }
122 Ok(Self {
123 customer_app,
124 services,
125 session_store_kind,
126 sessions,
127 })
128 }
129
130 pub fn session_store_kind(&self) -> SessionStoreBackendKind {
131 self.session_store_kind
132 }
133
134 pub fn session_store_is_shared(&self) -> bool {
135 self.sessions.is_shared()
136 }
137
138 pub fn issue_session(
139 &mut self,
140 request: SessionIssueRequest,
141 cookie_secret: &[u8],
142 now: BrowserInstant,
143 ) -> Result<IssuedBrowserSession, RuntimeBrowserError> {
144 issue_session(self, request, cookie_secret, now)
145 }
146
147 pub fn rotate_session(
148 &mut self,
149 session_id: &str,
150 cookie_secret: &[u8],
151 now: BrowserInstant,
152 ) -> Result<RotatedBrowserSession, RuntimeBrowserError> {
153 let session_id = validate_browser_value("session_id", session_id.to_string())?;
154 let existing = self.sessions.session(&session_id)?.ok_or_else(|| {
155 RuntimeBrowserError::UnknownSession {
156 session_id: session_id.clone(),
157 }
158 })?;
159 let principal_id = match existing.status_at(now) {
160 BrowserSessionStatus::Active => {
161 self.sessions.revoke(&session_id, now)?;
162 existing.principal_id.clone()
163 }
164 BrowserSessionStatus::IdleExpired | BrowserSessionStatus::AbsoluteExpired => {
165 self.sessions.delete(&session_id)?;
166 return Err(RuntimeBrowserError::ExpiredSession { session_id });
167 }
168 BrowserSessionStatus::Revoked => {
169 return Err(RuntimeBrowserError::RevokedSession { session_id });
170 }
171 };
172
173 let issued =
174 self.issue_session(SessionIssueRequest { principal_id }, cookie_secret, now)?;
175 Ok(RotatedBrowserSession {
176 previous_session_id: session_id,
177 issued,
178 })
179 }
180
181 pub fn revoke_session(
182 &mut self,
183 session_id: &str,
184 now: BrowserInstant,
185 ) -> Result<(), RuntimeBrowserError> {
186 let session_id = validate_browser_value("session_id", session_id.to_string())?;
187 self.sessions.revoke(&session_id, now)
188 }
189
190 pub fn issue_csrf_token(
191 &self,
192 csrf_secret: &[u8],
193 session_id: &str,
194 action: &str,
195 ) -> Result<String, RuntimeBrowserError> {
196 let session_id = validate_browser_value("session_id", session_id.to_string())?;
197 let action = validate_browser_value("action", action.to_string())?;
198 self.services
199 .csrf
200 .issue_token(csrf_secret, &session_id, &action)
201 .map_err(map_session_cookie_error)
202 }
203
204 pub fn issue_flash_cookie(
205 &self,
206 cookie_secret: &[u8],
207 messages: &[FlashMessage],
208 ) -> Result<String, RuntimeBrowserError> {
209 if messages.is_empty() {
210 return Err(RuntimeBrowserError::EmptyValue {
211 field: "flash_messages",
212 });
213 }
214
215 let payload = serialize_flash_messages(messages)?;
216 let value = self
217 .services
218 .sessions
219 .flash_cookie
220 .protect(cookie_secret, &payload)
221 .map_err(map_flash_cookie_error)?;
222 Ok(self
223 .services
224 .sessions
225 .flash_cookie
226 .render_set_cookie(&value, Some(Duration::from_secs(FLASH_COOKIE_MAX_AGE_SECS))))
227 }
228
229 pub fn clear_flash_cookie_header(&self) -> String {
230 self.services
231 .sessions
232 .flash_cookie
233 .render_set_cookie("", Some(Duration::from_secs(0)))
234 }
235
236 pub fn clear_session_cookie_header(&self) -> String {
237 self.services
238 .sessions
239 .session_cookie
240 .render_set_cookie("", Some(Duration::from_secs(0)))
241 }
242
243 pub fn session(
244 &self,
245 session_id: &str,
246 ) -> Result<Option<BrowserSessionRecord>, RuntimeBrowserError> {
247 self.sessions.session(session_id)
248 }
249
250 pub fn resolve_request(
251 &mut self,
252 request: &RequestInput,
253 cookie_secret: &[u8],
254 now: BrowserInstant,
255 ) -> Result<ResolvedBrowserRequest, RuntimeBrowserError> {
256 let mut response_cookies = Vec::new();
257 let flash_messages = match request.flash_cookie.as_deref() {
258 Some(cookie) => {
259 let messages = self.consume_flash_cookie(cookie_secret, cookie)?;
260 response_cookies.push(self.clear_flash_cookie_header());
261 messages
262 }
263 None => Vec::new(),
264 };
265
266 let mut resolved_from_cookie = false;
267 let session_id = if let Some(session_id) = request.session_id.as_ref() {
268 Some(validate_browser_value("session_id", session_id.clone())?)
269 } else if let Some(cookie) = request.session_cookie.as_deref() {
270 resolved_from_cookie = true;
271 Some(self.verify_session_cookie(cookie_secret, cookie)?)
272 } else {
273 None
274 };
275
276 let Some(session_id) = session_id else {
277 return Ok(ResolvedBrowserRequest {
278 session: SessionContext {
279 session_id: None,
280 resolved_from_cookie,
281 },
282 principal_id: None,
283 flash_messages,
284 response_cookies,
285 });
286 };
287
288 let (session_id, principal_id, refreshed_cookie) =
289 match self.touch_active_session(&session_id, cookie_secret, now) {
290 Ok((principal_id, refreshed_cookie)) => {
291 (session_id, principal_id, refreshed_cookie)
292 }
293 Err(RuntimeBrowserError::UnknownSession { .. }) if resolved_from_cookie => {
294 let issued =
295 self.issue_session(SessionIssueRequest::new(), cookie_secret, now)?;
296 (
297 issued.record.session_id.clone(),
298 issued.record.principal_id.clone(),
299 issued.set_cookie_header,
300 )
301 }
302 Err(error) => return Err(error),
303 };
304 response_cookies.push(refreshed_cookie);
305
306 Ok(ResolvedBrowserRequest {
307 session: SessionContext {
308 session_id: Some(session_id),
309 resolved_from_cookie,
310 },
311 principal_id,
312 flash_messages,
313 response_cookies,
314 })
315 }
316
317 pub(super) fn issue_cookie_for_record(
318 &self,
319 record: BrowserSessionRecord,
320 cookie_secret: &[u8],
321 ) -> Result<IssuedBrowserSession, RuntimeBrowserError> {
322 let cookie_value = self
323 .services
324 .sessions
325 .session_cookie
326 .protect(cookie_secret, &record.session_id)
327 .map_err(map_session_cookie_error)?;
328 let set_cookie_header = self
329 .services
330 .sessions
331 .session_cookie
332 .render_set_cookie(&cookie_value, Some(self.services.sessions.idle_timeout));
333 Ok(IssuedBrowserSession {
334 record,
335 cookie_value,
336 set_cookie_header,
337 })
338 }
339
340 fn verify_session_cookie(
341 &self,
342 cookie_secret: &[u8],
343 cookie: &str,
344 ) -> Result<String, RuntimeBrowserError> {
345 self.services
346 .sessions
347 .session_cookie
348 .unprotect(cookie_secret, cookie)
349 .map_err(map_session_cookie_error)
350 }
351
352 fn consume_flash_cookie(
353 &self,
354 cookie_secret: &[u8],
355 cookie: &str,
356 ) -> Result<Vec<FlashMessage>, RuntimeBrowserError> {
357 let payload = self
358 .services
359 .sessions
360 .flash_cookie
361 .unprotect(cookie_secret, cookie)
362 .map_err(map_flash_cookie_error)?;
363 deserialize_flash_messages(&payload)
364 }
365
366 fn touch_active_session(
367 &mut self,
368 session_id: &str,
369 cookie_secret: &[u8],
370 now: BrowserInstant,
371 ) -> Result<(Option<String>, String), RuntimeBrowserError> {
372 let principal_id = self.sessions.touch_active_session(
373 session_id,
374 self.services.sessions.idle_timeout,
375 now,
376 )?;
377 let cookie_value = self
378 .services
379 .sessions
380 .session_cookie
381 .protect(cookie_secret, session_id)
382 .map_err(map_session_cookie_error)?;
383 let cookie_header = self
384 .services
385 .sessions
386 .session_cookie
387 .render_set_cookie(&cookie_value, Some(self.services.sessions.idle_timeout));
388 Ok((principal_id, cookie_header))
389 }
390}