1use crate::entity::AuthSession;
2use crate::session::SessionRequest;
3use crate::session::cookie_cache as cache;
4use crate::session::cookie_cache::CacheValidation;
5use crate::session::cookie_cache::runtime::{
6 AuthenticatedRead, EstablishedSession, SessionHookCache, SessionHookCacheMetadata,
7 build_headers, cache_value, cookie_values, cookies, emit_issuance, establish, existing_names,
8 session_cleanup_headers, stored_read_headers,
9};
10use crate::utils::cookie_utils::{related_cookie_name, sign_cookie_value};
11use crate::utils::javascript::number_from_i64;
12use crate::{AuthContext, AuthError, AuthResult, AuthSchema, CacheVersionContext};
13use std::sync::Arc;
14pub async fn read<S: AuthSchema>(
17 ctx: &AuthContext<S>,
18 request: &impl SessionRequest,
19) -> AuthResult<Option<cache::CompactCache>> {
20 read_cache(ctx, request, CacheDecoding::Http).await
21}
22
23#[derive(Clone, Copy, PartialEq, Eq)]
25pub(crate) enum CacheDecoding {
26 Http,
28 MalformedIsMiss,
30}
31
32async fn read_cache<S: AuthSchema>(
33 ctx: &AuthContext<S>,
34 request: &impl SessionRequest,
35 decoding: CacheDecoding,
36) -> AuthResult<Option<cache::CompactCache>> {
37 let manager = ctx.session_manager();
38 let enabled = ctx
39 .config
40 .session
41 .cookie_cache
42 .as_ref()
43 .filter(|config| config.enabled);
44 let token = manager.extract_session_token(request);
45 if token.is_none() && enabled.is_some() {
46 return Ok(None);
47 }
48 let name = related_cookie_name(&ctx.config, "session_data");
49 let values = cookies(request.session_headers());
50 let chunks = cookie_values(request.session_headers(), true);
51 if enabled.is_none() {
52 for old in existing_names(&chunks, &name) {
53 request.queue_response_header(
54 "Set-Cookie",
55 cache::cookie_header(&old, "", Some(0.0), &ctx.config)?,
56 );
57 }
58 return Ok(None);
59 }
60 if request.session_query_truthy("disableCookieCache") {
61 return Ok(None);
62 }
63 let Some(token) = token else {
64 return Ok(None);
65 };
66 let Some(value) = cache_value(&values, &chunks, &name) else {
67 return Ok(None);
68 };
69 let config =
70 enabled.ok_or_else(|| AuthError::internal("Missing enabled cache configuration"))?;
71 let decoded = match config.strategy {
72 crate::CookieCacheStrategy::Compact => match decoding {
73 CacheDecoding::Http => cache::decode_compact_http(&value, ctx.config.current_secret())?,
74 CacheDecoding::MalformedIsMiss => {
75 cache::decode_compact(&value, ctx.config.current_secret())
76 }
77 },
78 crate::CookieCacheStrategy::Jwt => {
79 if let Some(signer) = ctx
80 .extensions
81 .get::<cache::jwt::CookieCacheSignerHandle<S>>()
82 {
83 signer
84 .0
85 .verify(&value, ctx)
86 .await?
87 .and_then(|claims| cache::jwt::decode_payload(&claims.into(), 15.0))
88 } else {
89 cache::jwt::decode(&value, ctx.config.current_secret())
90 }
91 }
92 crate::CookieCacheStrategy::Jwe => ctx
93 .config
94 .verification_secrets()
95 .find_map(|secret| {
96 crate::utils::jwe::decode(secret, "better-auth-session", &value).ok()
97 })
98 .and_then(|claims| cache::jwt::decode_payload(&claims.into(), 15.0)),
99 };
100 if let Some(cache) = decoded
101 && let CacheValidation::Hit(cache) = cache::validate_compact(
102 cache,
103 &token,
104 config.version.as_ref(),
105 chrono::Utc::now().timestamp_millis(),
106 )
107 .await?
108 {
109 return Ok(Some(*cache));
110 }
111 request.queue_response_header(
112 "Set-Cookie",
113 cache::cookie_header(&name, "", Some(0.0), &ctx.config)?,
114 );
115 Ok(None)
116}
117
118pub(crate) async fn renew_cache<S: AuthSchema>(
120 ctx: &AuthContext<S>,
121 request: &impl SessionRequest,
122 cache: &cache::CompactCache,
123) -> AuthResult<()> {
124 if ctx.config.session.stateless
125 && request
126 .extensions()
127 .get::<crate::session::SessionRefreshSuppressed>()
128 .is_none()
129 {
130 let config = ctx
131 .config
132 .session
133 .cookie_cache
134 .as_ref()
135 .ok_or_else(|| AuthError::internal("Missing stateless cache configuration"))?;
136 let update_age = match ctx.config.session.cookie_refresh_cache {
137 crate::CookieRefreshCache::Disabled => None,
138 crate::CookieRefreshCache::Automatic => {
139 Some((cache::effective_max_age(config.max_age) * 0.2).floor())
140 }
141 crate::CookieRefreshCache::UpdateAge(age) => Some(age),
142 };
143 if update_age.is_some_and(|age| {
147 cache.expires_at - number_from_i64(chrono::Utc::now().timestamp_millis()) < age * 1000.0
148 }) {
149 let context = CacheVersionContext::cached(cache.user.clone(), cache.session.clone());
150 for header in
151 build_headers(ctx, context, request.session_headers(), false, None).await?
152 {
153 request.queue_response_header("Set-Cookie", header);
154 }
155 let remember = ctx.session_manager().has_dont_remember_cookie(request);
156 request.queue_response_header(
157 "Set-Cookie",
158 cache::cookie_header(
159 &ctx.config.session.cookie_name,
160 &percent_encoding::percent_decode_str(&sign_cookie_value(
161 &cache.session.token,
162 ctx.config.current_secret(),
163 ))
164 .decode_utf8_lossy(),
165 (!remember)
166 .then_some(number_from_i64(ctx.config.session.expires_in.num_seconds())),
167 &ctx.config,
168 )?,
169 );
170 }
171 }
172 Ok(())
173}
174
175pub async fn authenticated<S: AuthSchema>(
178 ctx: &AuthContext<S>,
179 request: &impl SessionRequest,
180 direct: bool,
181) -> AuthResult<Option<AuthenticatedRead<S>>> {
182 authenticated_with(ctx, request, direct, CacheDecoding::Http).await
183}
184
185pub(crate) async fn authenticated_with<S: AuthSchema>(
186 ctx: &AuthContext<S>,
187 request: &impl SessionRequest,
188 direct: bool,
189 decoding: CacheDecoding,
190) -> AuthResult<Option<AuthenticatedRead<S>>> {
191 if !direct && let Some(call) = request.endpoint_call() {
192 let nested = call.session_read_context();
193 crate::endpoint::with_endpoint_call_context(
194 nested.clone(),
195 Box::pin(authenticated_inner(ctx, &nested, false, decoding)),
196 )
197 .await
198 } else {
199 Box::pin(authenticated_inner(ctx, request, direct, decoding)).await
200 }
201}
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 mut user = ctx.database.get_session_user_record(&token).await?;
272 let secondary_only = ctx.config.session.secondary_storage.is_some()
273 && (!ctx.config.session.store_in_database || ctx.config.session.preserve_in_database);
274 if user.is_none() && !secondary_only {
275 user = ctx
276 .database
277 .get_user_by_id_record(original.user_id().as_ref())
278 .await?;
279 }
280 let Some(user) = user else {
281 cleanup(ctx, request)?;
282 return Ok(None);
283 };
284 request.set_session_hook_snapshot(ctx.user_view(&user), ctx.session_view(&original));
285 let suppressed = manager.request_disables_refresh(request);
286 let skip = request
287 .extensions()
288 .get::<crate::session::SessionRefreshSuppressed>()
289 .is_some();
290 let deferred = ctx.config.session.defer_session_refresh
291 && !(direct && request.session_method() == &crate::HttpMethod::Post);
292 let read = manager
293 .read_loaded_session_record(
294 original,
295 crate::session::SessionReadOptions {
296 allow_refresh: !suppressed && !deferred && !skip,
297 cleanup_expired: !deferred,
298 },
299 )
300 .await?;
301 let Some(session) = read.session else {
302 cleanup(ctx, request)?;
303 if read.needs_refresh {
304 return Err(AuthError::Upstream {
305 status: 401,
306 code: "FAILED_TO_GET_SESSION",
307 message: "Failed to get session",
308 });
309 }
310 return Ok(None);
311 };
312 if read.refreshed {
313 request.queue_response_header(
314 "Set-Cookie",
315 cache::cookie_header(
316 &ctx.config.session.cookie_name,
317 &percent_encoding::percent_decode_str(&sign_cookie_value(
318 session.token(),
319 ctx.config.current_secret(),
320 ))
321 .decode_utf8_lossy(),
322 Some(number_from_i64(ctx.config.session.expires_in.num_seconds())),
323 &ctx.config,
324 )?,
325 );
326 let user = ctx.filter_user_record(user.clone());
327 emit_issuance(ctx, &user, &session).await?;
328 } else if !suppressed {
329 for header in stored_read_headers(ctx, &user, &session, request.session_headers()).await? {
330 request.queue_response_header("Set-Cookie", header);
331 }
332 }
333 Ok(Some(establish(
334 ctx,
335 request,
336 AuthenticatedRead {
337 user: crate::AuthenticatedUser::Stored(user),
338 session: ctx.session_view(&session),
339 needs_refresh: (deferred && !suppressed).then_some(read.needs_refresh && !skip),
340 },
341 )))
342}
343
344pub fn cleanup<S: AuthSchema>(
350 ctx: &AuthContext<S>,
351 request: &impl SessionRequest,
352) -> AuthResult<()> {
353 let cache_name = related_cookie_name(&ctx.config, "session_data");
354 let mut names = vec![ctx.config.session.cookie_name.clone(), cache_name];
355 if ctx.config.account.store_account_cookie {
356 names.push(related_cookie_name(&ctx.config, "account_data"));
357 }
358 if matches!(
359 ctx.config.account.store_state_strategy,
360 crate::config::OAuthStateStrategy::Cookie
361 ) {
362 names.push(related_cookie_name(&ctx.config, "oauth_state"));
363 }
364 let prior = request.take_response_headers();
367 let remember_name = related_cookie_name(&ctx.config, "dont_remember");
368 for (name, value) in prior {
369 let replaced = name.eq_ignore_ascii_case("set-cookie")
370 && names
371 .iter()
372 .chain(std::iter::once(&remember_name))
373 .any(|cookie_name| {
374 value
375 .strip_prefix(cookie_name.as_str())
376 .is_some_and(|rest| rest.starts_with(['=', '.']))
377 });
378 if !replaced {
379 request.queue_response_header(name, value);
380 }
381 }
382 for header in session_cleanup_headers(&ctx.config, request.session_headers(), false)? {
383 request.queue_response_header("Set-Cookie", header);
384 }
385 Ok(())
386}