alibi_core/session/cookie_cache/runtime/
read.rs1use super::*;
2pub async fn read<S: AuthSchema>(
8 ctx: &AuthContext<S>,
9 request: &impl SessionRequest,
10) -> AuthResult<Option<super::super::CompactCache>> {
11 read_cache(ctx, request, CacheDecoding::Http).await
12}
13
14#[derive(Clone, Copy, PartialEq, Eq)]
16pub(crate) enum CacheDecoding {
17 Http,
19 MalformedIsMiss,
21}
22
23async fn read_cache<S: AuthSchema>(
24 ctx: &AuthContext<S>,
25 request: &impl SessionRequest,
26 decoding: CacheDecoding,
27) -> AuthResult<Option<super::super::CompactCache>> {
28 let manager = ctx.session_manager();
29 let enabled = ctx
30 .config
31 .session
32 .cookie_cache
33 .as_ref()
34 .filter(|config| config.enabled);
35 let token = manager.extract_session_token(request);
36 if token.is_none() && enabled.is_some() {
37 return Ok(None);
38 }
39 let name = related_cookie_name(&ctx.config, "session_data");
40 let values = cookies(request.session_headers());
41 let chunks = cookie_values(request.session_headers(), true);
42 if enabled.is_none() {
43 for old in existing_names(&chunks, &name) {
44 request.queue_response_header(
45 "Set-Cookie",
46 super::super::cookie_header(&old, "", Some(0.0), &ctx.config)?,
47 );
48 }
49 return Ok(None);
50 }
51 if request.session_query_truthy("disableCookieCache") {
52 return Ok(None);
53 }
54 let Some(token) = token else {
55 return Ok(None);
56 };
57 let Some(value) = cache_value(&values, &chunks, &name) else {
58 return Ok(None);
59 };
60 let config =
61 enabled.ok_or_else(|| AuthError::internal("Missing enabled cache configuration"))?;
62 let decoded = match config.strategy {
63 crate::CookieCacheStrategy::Compact => match decoding {
64 CacheDecoding::Http => {
65 super::super::decode_compact_http(&value, ctx.config.current_secret())?
66 }
67 CacheDecoding::MalformedIsMiss => {
68 super::super::decode_compact(&value, ctx.config.current_secret())
69 }
70 },
71 crate::CookieCacheStrategy::Jwt => {
72 if let Some(signer) = ctx
73 .extensions
74 .get::<super::super::jwt::CookieCacheSignerHandle<S>>()
75 {
76 signer
77 .0
78 .verify(&value, ctx)
79 .await?
80 .and_then(|claims| super::super::jwt::decode_payload(&claims.into(), 15.0))
81 } else {
82 super::super::jwt::decode(&value, ctx.config.current_secret())
83 }
84 }
85 crate::CookieCacheStrategy::Jwe => ctx
86 .config
87 .verification_secrets()
88 .find_map(|secret| {
89 crate::utils::jwe::decode(secret, "better-auth-session", &value).ok()
90 })
91 .and_then(|claims| super::super::jwt::decode_payload(&claims.into(), 15.0)),
92 };
93 if let Some(cache) = decoded
94 && let CacheValidation::Hit(cache) = super::super::validate_compact(
95 cache,
96 &token,
97 config.version.as_ref(),
98 chrono::Utc::now().timestamp_millis(),
99 )
100 .await?
101 {
102 return Ok(Some(*cache));
103 }
104 request.queue_response_header(
105 "Set-Cookie",
106 super::super::cookie_header(&name, "", Some(0.0), &ctx.config)?,
107 );
108 Ok(None)
109}
110
111pub(crate) async fn renew_cache<S: AuthSchema>(
113 ctx: &AuthContext<S>,
114 request: &impl SessionRequest,
115 cache: &super::super::CompactCache,
116) -> AuthResult<()> {
117 if ctx.config.session.stateless
118 && request
119 .extensions()
120 .get::<crate::session::SessionRefreshSuppressed>()
121 .is_none()
122 {
123 let config = ctx
124 .config
125 .session
126 .cookie_cache
127 .as_ref()
128 .ok_or_else(|| AuthError::internal("Missing stateless cache configuration"))?;
129 let update_age = match ctx.config.session.cookie_refresh_cache {
130 crate::CookieRefreshCache::Disabled => None,
131 crate::CookieRefreshCache::Automatic => {
132 Some((super::super::effective_max_age(config.max_age) * 0.2).floor())
133 }
134 crate::CookieRefreshCache::UpdateAge(age) => Some(age),
135 };
136 if update_age.is_some_and(|age| {
140 cache.expires_at - (chrono::Utc::now().timestamp_millis() as f64) < age * 1000.0
141 }) {
142 let context = CacheVersionContext::cached(cache.user.clone(), cache.session.clone());
143 for header in
144 build_headers(ctx, context, request.session_headers(), false, None).await?
145 {
146 request.queue_response_header("Set-Cookie", header);
147 }
148 let remember = ctx.session_manager().has_dont_remember_cookie(request);
149 request.queue_response_header(
150 "Set-Cookie",
151 super::super::cookie_header(
152 &ctx.config.session.cookie_name,
153 &percent_encoding::percent_decode_str(&sign_cookie_value(
154 &cache.session.token,
155 ctx.config.current_secret(),
156 ))
157 .decode_utf8_lossy(),
158 (!remember).then_some(ctx.config.session.expires_in.num_seconds() as f64),
159 &ctx.config,
160 )?,
161 );
162 }
163 }
164 Ok(())
165}
166
167pub async fn authenticated<S: AuthSchema>(
173 ctx: &AuthContext<S>,
174 request: &impl SessionRequest,
175 direct: bool,
176) -> AuthResult<Option<AuthenticatedRead<S>>> {
177 authenticated_with(ctx, request, direct, CacheDecoding::Http).await
178}
179
180pub(crate) async fn authenticated_with<S: AuthSchema>(
181 ctx: &AuthContext<S>,
182 request: &impl SessionRequest,
183 direct: bool,
184 decoding: CacheDecoding,
185) -> AuthResult<Option<AuthenticatedRead<S>>> {
186 if !direct && let Some(call) = request.endpoint_call() {
187 let nested = call.session_read_context();
188 crate::endpoint::with_endpoint_call_context(
189 nested.clone(),
190 Box::pin(authenticated_inner(ctx, &nested, false, decoding)),
191 )
192 .await
193 } else {
194 Box::pin(authenticated_inner(ctx, request, direct, decoding)).await
195 }
196}
197
198#[expect(
199 clippy::as_conversions,
200 clippy::cast_precision_loss,
201 reason = "Preserve JavaScript Number rounding at the compatibility boundary"
202)]
203pub(in crate::session::cookie_cache::runtime) async fn authenticated_inner<S: AuthSchema>(
204 ctx: &AuthContext<S>,
205 request: &impl SessionRequest,
206 direct: bool,
207 decoding: CacheDecoding,
208) -> AuthResult<Option<AuthenticatedRead<S>>> {
209 if let Some(established) = request.extensions().get::<EstablishedSession<S>>()
210 && let Some(established) = &established.0
211 && Arc::ptr_eq(&established.config, &ctx.config)
212 && Arc::ptr_eq(&established.database, &ctx.database)
213 && &established.headers == request.session_headers()
214 && established.virtual_session == request.virtual_session(ctx)
215 {
216 let read = &established.read;
217 return Ok(Some(AuthenticatedRead {
218 user: read.user.clone(),
219 session: read.session.clone(),
220 needs_refresh: read.needs_refresh,
221 }));
222 }
223 request.extensions().insert(SessionHookCache(None));
224 if let Some(session) = request.virtual_session(ctx) {
225 let user = if let Some(user) = request.authenticated_user::<S>(ctx) {
226 ctx.user_adapter_record(user).await?
227 } else {
228 let Some(user) = ctx.database.get_user_by_id_record(&session.user_id).await? else {
229 return Ok(None);
230 };
231 user
232 };
233 request.set_session_hook_snapshot(ctx.user_view(&user), session.clone());
234 return Ok(Some(establish(
235 ctx,
236 request,
237 AuthenticatedRead {
238 user: crate::AuthenticatedUser::Stored(user),
239 session,
240 needs_refresh: None,
241 },
242 )));
243 }
244 if let Some(cache) = read_cache(ctx, request, decoding).await? {
245 renew_cache(ctx, request, &cache).await?;
246 request
247 .extensions()
248 .insert(SessionHookCache(Some(SessionHookCacheMetadata {
249 updated_at: cache.updated_at,
250 version: cache.version.clone(),
251 })));
252 request.set_session_hook_snapshot(cache.user.clone(), cache.session.clone());
253 return Ok(Some(establish(
254 ctx,
255 request,
256 AuthenticatedRead {
257 user: crate::AuthenticatedUser::Cached(Box::new(cache.user)),
258 session: cache.session,
259 needs_refresh: None,
260 },
261 )));
262 }
263 let manager = ctx.session_manager();
264 let Some(token) = manager.extract_session_token(request) else {
265 return Ok(None);
266 };
267 let Some(original) = ctx.database.get_session_record(&token).await? else {
268 cleanup(ctx, request)?;
269 return Ok(None);
270 };
271 let user = match ctx.database.get_session_user_record(&token).await? {
272 Some(user) => Some(user),
273 None if ctx.config.session.secondary_storage.is_some()
274 && (!ctx.config.session.store_in_database
275 || ctx.config.session.preserve_in_database) =>
276 {
277 None
278 }
279 None => {
280 ctx.database
281 .get_user_by_id_record(original.user_id().as_ref())
282 .await?
283 }
284 };
285 let Some(user) = user else {
286 cleanup(ctx, request)?;
287 return Ok(None);
288 };
289 request.set_session_hook_snapshot(ctx.user_view(&user), ctx.session_view(&original));
290 let suppressed = manager.request_disables_refresh(request);
291 let skip = request
292 .extensions()
293 .get::<crate::session::SessionRefreshSuppressed>()
294 .is_some();
295 let deferred = ctx.config.session.defer_session_refresh
296 && !(direct && request.session_method() == &crate::HttpMethod::Post);
297 let read = manager
298 .read_loaded_session_record(
299 original,
300 crate::session::SessionReadOptions {
301 allow_refresh: !suppressed && !deferred && !skip,
302 cleanup_expired: !deferred,
303 },
304 )
305 .await?;
306 let Some(session) = read.session else {
307 cleanup(ctx, request)?;
308 if read.needs_refresh {
309 return Err(AuthError::Upstream {
310 status: 401,
311 code: "FAILED_TO_GET_SESSION",
312 message: "Failed to get session",
313 });
314 }
315 return Ok(None);
316 };
317 if read.refreshed {
318 request.queue_response_header(
319 "Set-Cookie",
320 super::super::cookie_header(
321 &ctx.config.session.cookie_name,
322 &percent_encoding::percent_decode_str(&sign_cookie_value(
323 session.token(),
324 ctx.config.current_secret(),
325 ))
326 .decode_utf8_lossy(),
327 Some(ctx.config.session.expires_in.num_seconds() as f64),
328 &ctx.config,
329 )?,
330 );
331 }
332 if read.refreshed {
333 let user = ctx.filter_user_record(user.clone());
334 emit_issuance(ctx, &user, &session).await?;
335 } else if !suppressed {
336 for header in stored_read_headers(ctx, &user, &session, request.session_headers()).await? {
337 request.queue_response_header("Set-Cookie", header);
338 }
339 }
340 Ok(Some(establish(
341 ctx,
342 request,
343 AuthenticatedRead {
344 user: crate::AuthenticatedUser::Stored(user),
345 session: ctx.session_view(&session),
346 needs_refresh: (deferred && !suppressed).then_some(read.needs_refresh && !skip),
347 },
348 )))
349}
350
351pub(in crate::session::cookie_cache::runtime) fn cleanup<S: AuthSchema>(
352 ctx: &AuthContext<S>,
353 request: &impl SessionRequest,
354) -> AuthResult<()> {
355 let cache_name = related_cookie_name(&ctx.config, "session_data");
356 let mut names = vec![ctx.config.session.cookie_name.clone(), cache_name.clone()];
357 if ctx.config.account.store_account_cookie {
358 names.push(related_cookie_name(&ctx.config, "account_data"));
359 }
360 if matches!(
361 ctx.config.account.store_state_strategy,
362 crate::config::OAuthStateStrategy::Cookie
363 ) {
364 names.push(related_cookie_name(&ctx.config, "oauth_state"));
365 }
366 let prior = request.take_response_headers();
369 let remember_name = related_cookie_name(&ctx.config, "dont_remember");
370 for (name, value) in prior {
371 let replaced = name.eq_ignore_ascii_case("set-cookie")
372 && names
373 .iter()
374 .chain(std::iter::once(&remember_name))
375 .any(|cookie_name| {
376 value.starts_with(&format!("{cookie_name}="))
377 || value.starts_with(&format!("{cookie_name}."))
378 });
379 if !replaced {
380 request.queue_response_header(name, value);
381 }
382 }
383 for header in session_cleanup_headers(&ctx.config, request.session_headers(), false)? {
384 request.queue_response_header("Set-Cookie", header);
385 }
386 Ok(())
387}