1use chrono::Utc;
2use std::sync::Arc;
3
4use crate::config::AuthConfig;
5use crate::entity::{AuthSession, AuthUser};
6use crate::error::AuthResult;
7use crate::schema::AuthSchema;
8use crate::store::{AuthStore, JoinValue};
9use crate::types::CreateSession;
10use crate::utils::cookie_utils::{
11 get_cookie, related_cookie_name, session_cookie_headers, verify_cookie_value,
12};
13use crate::wire::{SessionView, UserView};
14use crate::{AuthError, AuthRequest, HttpMethod};
15use serde::{Deserialize, Serialize};
16
17#[cfg(test)]
18mod cache_tests;
19mod cookie_cache;
20mod native;
21mod response;
22mod signer;
23pub use native::NativeSessionData;
24pub use signer::{SessionCookieContext, SessionCookieSigner};
25#[cfg(test)]
26mod response_tests;
27mod view;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct SessionData<U = UserView> {
32 pub session: SessionView,
34 pub user: U,
36}
37
38pub struct SessionResolution<T = SessionData> {
40 pub data: Option<T>,
42 pub needs_refresh: Option<bool>,
44}
45
46#[derive(Clone, Copy)]
48pub enum SessionRead {
49 Cached,
51 Authoritative,
53}
54
55pub struct SessionManager<S: AuthSchema> {
57 capabilities: crate::store::StoreCapabilities,
58 user_metadata: crate::plugin::MetadataMap,
59 adapter_user_fields: crate::user_fields::UserConfig,
60 config: Arc<AuthConfig>,
61 database: Arc<dyn AuthStore<S>>,
62 signer: Option<Arc<dyn SessionCookieSigner<S>>>,
63}
64
65impl<S: AuthSchema> Clone for SessionManager<S> {
66 fn clone(&self) -> Self {
67 Self {
68 capabilities: self.capabilities,
69 user_metadata: self.user_metadata.clone(),
70 adapter_user_fields: self.adapter_user_fields.clone(),
71 config: self.config.clone(),
72 database: self.database.clone(),
73 signer: self.signer.clone(),
74 }
75 }
76}
77
78impl<S: AuthSchema> SessionManager<S> {
79 pub fn new(config: Arc<AuthConfig>, database: Arc<dyn AuthStore<S>>) -> Self {
80 Self {
81 capabilities: Default::default(),
82 adapter_user_fields: config.user.clone(),
83 config,
84 database,
85 user_metadata: Default::default(),
86 signer: None,
87 }
88 }
89
90 pub fn with_adapter_user_fields(mut self, fields: crate::user_fields::UserConfig) -> Self {
92 self.adapter_user_fields = fields;
93 self
94 }
95
96 pub fn with_user_metadata(mut self, metadata: crate::plugin::MetadataMap) -> Self {
98 self.user_metadata = metadata;
99 self
100 }
101
102 pub(crate) fn with_store_capabilities(
103 mut self,
104 capabilities: crate::store::StoreCapabilities,
105 ) -> Self {
106 self.capabilities = capabilities;
107 self
108 }
109
110 async fn user_view(&self, user: &impl AuthUser) -> AuthResult<UserView> {
111 UserView::with_field_policies(
112 user,
113 &self.adapter_user_fields,
114 &self.config.user,
115 &self.user_metadata,
116 self.database.supports_native_json(),
117 )
118 .await
119 }
120
121 pub async fn session_view(&self, session: &impl AuthSession) -> AuthResult<SessionView> {
122 let mut view = self.internal_session_view(session).await?;
123 view.filter_returned_fields(&self.config.session);
124 Ok(view)
125 }
126
127 pub async fn internal_session_view(
129 &self,
130 session: &impl AuthSession,
131 ) -> AuthResult<SessionView> {
132 let mut view = SessionView::with_internal_fields_for_adapter(
133 session,
134 &self.config.session,
135 self.database.supports_native_json(),
136 )
137 .await?;
138 view.visible_fields = Some(
139 SessionView::active_plugin_fields(&self.user_metadata)
140 .filter(|name| {
141 session
142 .field_presence()
143 .is_none_or(|fields| fields.contains(*name))
144 })
145 .map(str::to_owned)
146 .collect(),
147 );
148 Ok(view)
149 }
150
151 async fn internal_user_view(&self, user: &impl AuthUser) -> AuthResult<UserView> {
152 UserView::with_internal_fields_for_adapter(
153 user,
154 &self.adapter_user_fields,
155 &self.user_metadata,
156 self.database.supports_native_json(),
157 )
158 .await
159 }
160
161 pub async fn internal_data(
163 &self,
164 user: &impl AuthUser,
165 session: &impl AuthSession,
166 ) -> AuthResult<SessionData> {
167 Ok(SessionData {
168 user: self.internal_user_view(user).await?,
169 session: self.internal_session_view(session).await?,
170 })
171 }
172
173 pub async fn set_session_cookie(
175 &self,
176 req: &AuthRequest,
177 data: SessionData,
178 dont_remember: Option<bool>,
179 ) -> AuthResult<()> {
180 self.set_native_session_cookie(req, data.into(), dont_remember)
181 .await
182 }
183
184 pub async fn set_native_session_cookie(
186 &self,
187 req: &AuthRequest,
188 data: NativeSessionData,
189 dont_remember: Option<bool>,
190 ) -> AuthResult<()> {
191 self.issue_session_cookie(req, data, dont_remember, None)
192 .await
193 }
194
195 pub async fn set_session_cookie_in_transaction(
197 &self,
198 req: &AuthRequest,
199 data: SessionData,
200 dont_remember: Option<bool>,
201 transaction: &dyn crate::store::AuthTransaction<S>,
202 ) -> AuthResult<()> {
203 self.issue_session_cookie(req, data.into(), dont_remember, Some(transaction))
204 .await
205 }
206
207 async fn issue_session_cookie(
208 &self,
209 req: &AuthRequest,
210 data: NativeSessionData,
211 dont_remember: Option<bool>,
212 transaction: Option<&dyn crate::store::AuthTransaction<S>>,
213 ) -> AuthResult<()> {
214 let dont_remember = dont_remember.unwrap_or_else(|| self.dont_remember(req));
215 for cookie in session_cookie_headers(&data.session.token, dont_remember, &self.config) {
216 req.append_response_header("Set-Cookie", cookie?)?;
217 }
218 self.write_cache_with_response(req, &data, dont_remember, None, transaction)
219 .await?;
220 req.set_new_session(data)
221 }
222
223 fn public_data(
224 &self,
225 mut data: SessionData<JoinValue<UserView>>,
226 ) -> SessionData<JoinValue<UserView>> {
227 if let JoinValue::One(Some(user)) = &mut data.user {
228 user.filter_cached_fields(&self.config.user);
229 }
230 data.session.filter_returned_fields(&self.config.session);
231 data
232 }
233
234 pub fn with_cookie_signer(mut self, signer: Option<Arc<dyn SessionCookieSigner<S>>>) -> Self {
236 self.signer = signer;
237 self
238 }
239
240 pub async fn create_session(
242 &self,
243 user: &impl AuthUser,
244 ip_address: Option<String>,
245 user_agent: Option<String>,
246 ) -> AuthResult<crate::wire::SessionView> {
247 self.create_session_for_id(user.id().into_owned(), ip_address, user_agent)
248 .await
249 }
250
251 pub async fn create_session_for_id(
253 &self,
254 user_id: crate::SchemaValue<String>,
255 ip_address: Option<String>,
256 user_agent: Option<String>,
257 ) -> AuthResult<SessionView> {
258 self.create_session_for_id_with_lifetime(
259 user_id,
260 ip_address,
261 user_agent,
262 self.config.session.expires_in(),
263 )
264 .await
265 }
266
267 pub async fn create_session_with_lifetime(
269 &self,
270 user: &impl AuthUser,
271 ip_address: Option<String>,
272 user_agent: Option<String>,
273 expires_in: chrono::Duration,
274 ) -> AuthResult<crate::wire::SessionView> {
275 self.create_session_for_id_with_lifetime(
276 user.id().into_owned(),
277 ip_address,
278 user_agent,
279 expires_in,
280 )
281 .await
282 }
283
284 pub async fn create_session_for_id_with_lifetime(
286 &self,
287 user_id: crate::SchemaValue<String>,
288 ip_address: Option<String>,
289 user_agent: Option<String>,
290 expires_in: chrono::Duration,
291 ) -> AuthResult<SessionView> {
292 let expires_at = Utc::now() + expires_in;
293
294 let create_session = CreateSession {
295 additional_fields: Default::default(),
296 user_id,
297 expires_at: expires_at.into(),
298 ip_address,
299 user_agent,
300 impersonated_by: None,
301 active_organization_id: None,
302 };
303
304 let session = self.database.create_session(create_session).await?;
305 Ok(session)
306 }
307
308 pub async fn get_session(&self, token: &str) -> AuthResult<Option<crate::wire::SessionView>> {
310 let Some(session) = self.database.get_session(token).await? else {
311 return Ok(None);
312 };
313 if session.expires_at().milliseconds() < Utc::now().timestamp_millis() as f64
314 || !session.active()
315 {
316 self.database.delete_session(token).await?;
317 return Ok(None);
318 }
319 if self.needs_refresh(&session) {
320 let new_expires_at = Utc::now() + self.config.session.expires_in();
321 return self
322 .database
323 .update_session_expiry(token, new_expires_at)
324 .await
325 .map(Some);
326 }
327 Ok(Some(session))
328 }
329
330 fn needs_refresh(&self, session: &impl AuthSession) -> bool {
331 !self.config.session.disable_session_refresh()
332 && session.expires_at().milliseconds()
333 - self.config.session.expires_in().num_milliseconds() as f64
334 + self.config.session.update_age().num_milliseconds() as f64
335 <= Utc::now().timestamp_millis() as f64
336 }
337
338 pub async fn resolve(
340 &self,
341 req: &AuthRequest,
342 read: SessionRead,
343 ) -> AuthResult<SessionResolution> {
344 let resolved = self.resolve_relations(req, read, true).await?;
345 Ok(SessionResolution {
346 data: resolved
347 .data
348 .map(SessionData::into_typed)
349 .transpose()?
350 .flatten(),
351 needs_refresh: resolved.needs_refresh,
352 })
353 }
354
355 pub async fn resolve_native(
357 &self,
358 req: &AuthRequest,
359 read: SessionRead,
360 ) -> AuthResult<SessionResolution<NativeSessionData>> {
361 let resolved = self.resolve_relations(req, read, false).await?;
362 Ok(SessionResolution {
363 data: resolved.data.map(Into::into),
364 needs_refresh: resolved.needs_refresh,
365 })
366 }
367
368 async fn resolve_relations(
369 &self,
370 req: &AuthRequest,
371 read: SessionRead,
372 typed_user: bool,
373 ) -> AuthResult<SessionResolution<SessionData<JoinValue<UserView>>>> {
374 let raw = if req.path() == "/get-session" {
375 req.query.clone()
376 } else {
377 Some(
378 req.query
379 .as_ref()
380 .filter(|value| value.is_object())
381 .cloned()
382 .unwrap_or_else(|| serde_json::json!({})),
383 )
384 };
385 let query = crate::query::session_query(raw)?;
386 crate::query::with_validated_query(query, self.resolve_inner(req, read, typed_user)).await
387 }
388
389 async fn resolve_inner(
390 &self,
391 req: &AuthRequest,
392 read: SessionRead,
393 typed_user: bool,
394 ) -> AuthResult<SessionResolution<SessionData<JoinValue<UserView>>>> {
395 let none = || SessionResolution {
396 data: None,
397 needs_refresh: None,
398 };
399 if let Some(session) = req.virtual_session() {
400 let Some(user_id) = session.user_id.as_str() else {
401 return Ok(none());
402 };
403 let user = self
404 .database
405 .get_user_by_id(user_id)
406 .await?
407 .ok_or(AuthError::UserNotFound)?;
408 let data = SessionData {
409 session: session.clone(),
410 user: self.user_view(&user).await?,
411 };
412 req.set_session_snapshot(Some(data.clone().into()))?;
413 return Ok(SessionResolution {
414 data: Some(data.into()),
415 needs_refresh: None,
416 });
417 }
418 let cache = self
419 .config
420 .session
421 .cookie_cache
422 .as_ref()
423 .filter(|cache| cache.enabled());
424 let token = self.extract_session_token(req);
425 if token.is_none() && cache.is_some() {
426 return Ok(none());
427 }
428 let cache_value =
429 cookie_cache::read(req, &related_cookie_name(&self.config, "session_data"));
430 if cache.is_none() && cache_value.is_some() {
431 cookie_cache::clear_existing(req, &self.config)?;
432 }
433 let Some(token) = token else {
434 return Ok(none());
435 };
436 let disable_cache = (matches!(read, SessionRead::Authoritative)
437 && self.capabilities.server_sessions())
438 || query_flag(req, "disableCookieCache")?;
439 if !disable_cache && let (Some(cache), Some(value)) = (cache, cache_value.as_deref()) {
440 let decoded = if let Some(signer) = &self.signer {
441 signer
442 .verify(
443 value,
444 SessionCookieContext {
445 request: req,
446 config: &self.config,
447 transaction: None,
448 },
449 )
450 .await?
451 .and_then(cookie_cache::parse_jwt)
452 } else {
453 cookie_cache::decode(value, &self.config, cache)
454 };
455 if let Some((mut payload, expires)) = decoded
456 && payload.data.session.token == token
457 && (if payload.version.is_empty() {
458 "1"
459 } else {
460 &payload.version
461 }) == cache.version.resolve(&payload.data.clone().into()).await?
462 && expires >= Utc::now().timestamp_millis()
463 && payload.data.session.expires_at.milliseconds()
464 >= Utc::now().timestamp_millis() as f64
465 {
466 if !self.capabilities.server_sessions()
467 && let Some(update_age) = cache.refresh_age()
468 && expires - Utc::now().timestamp_millis() < update_age.num_milliseconds()
469 {
470 self.write_cache(req, &payload.data, false).await?;
471 let max_age = (!self.dont_remember(req))
472 .then_some(self.config.session.expires_in().as_seconds_f64());
473 req.append_response_header(
474 "Set-Cookie",
475 crate::utils::cookie_utils::create_session_cookie_with_max_age(
476 Some(&payload.data.session.token),
477 max_age,
478 &self.config,
479 )?,
480 )?;
481 }
482 let raw_snapshot =
483 self.capabilities.server_sessions() || cache.refresh_age().is_none();
484 if raw_snapshot {
485 req.set_session_snapshot(Some(payload.data.clone().into()))?;
486 }
487 payload.data.user.filter_cached_fields(&self.config.user);
488 payload
489 .data
490 .session
491 .filter_returned_fields(&self.config.session);
492 if !raw_snapshot {
493 req.set_session_snapshot(Some(payload.data.clone().into()))?;
494 }
495 return Ok(SessionResolution {
496 data: Some(payload.data.into()),
497 needs_refresh: None,
498 });
499 }
500 cookie_cache::expire(req, &self.config)?;
501 }
502 let is_post = req.path().ends_with("/get-session") && req.method() == &HttpMethod::Post;
503 let stored = self.database.get_session_snapshot(&token).await?;
504 let Some((session, cached_data)) = stored else {
505 req.set_session_snapshot(None)?;
506 self.clear_cookies(req)?;
507 return Ok(none());
508 };
509 let mut data = if let Some(mut data) = cached_data {
510 match &mut data.user {
511 JoinValue::One(Some(user)) => *user = self.user_view(user).await?,
512 JoinValue::One(None) => {
513 req.set_session_snapshot(None)?;
514 self.clear_cookies(req)?;
515 return Ok(none());
516 }
517 JoinValue::Many(_) if typed_user => return Err(relationship_array_error()),
518 JoinValue::Many(_) => {}
519 }
520 data.session.filter_returned_fields(&self.config.session);
521 data
522 } else {
523 let Some(user_id) = session.user_id.as_str() else {
524 return Ok(none());
525 };
526 let Some(user) = self.database.get_user_by_id(user_id).await? else {
527 req.set_session_snapshot(None)?;
528 self.clear_cookies(req)?;
529 return Ok(none());
530 };
531 SessionData {
532 session: self.session_view(&session).await?,
533 user: JoinValue::One(Some(self.user_view(&user).await?)),
534 }
535 };
536 req.set_session_snapshot(Some(data.clone()))?;
537 if session.expires_at().milliseconds() < Utc::now().timestamp_millis() as f64
538 || !session.active()
539 {
540 self.clear_cookies(req)?;
541 if !self.config.session.defer_session_refresh || is_post {
542 self.database.delete_session(&token).await?;
543 }
544 return Ok(none());
545 }
546 let dont_remember = self.dont_remember(req);
547 if dont_remember || query_flag(req, "disableRefresh")? {
548 return Ok(SessionResolution {
549 data: Some(self.public_data(data)),
550 needs_refresh: None,
551 });
552 }
553 let needs_refresh = self.needs_refresh(&session);
554 if self.config.session.defer_session_refresh && !is_post {
555 self.write_cache_with_response(req, &data.clone().into(), false, None, None)
556 .await?;
557 return Ok(SessionResolution {
558 data: Some(self.public_data(data)),
559 needs_refresh: Some(needs_refresh),
560 });
561 }
562 if needs_refresh {
563 let updated = match self
564 .database
565 .update_session_expiry(&token, Utc::now() + self.config.session.expires_in())
566 .await
567 {
568 Err(AuthError::SessionNotFound) => {
569 self.clear_cookies(req)?;
570 return Err(failed_session_update());
571 }
572 result => result?,
573 };
574 data.session = self.internal_session_view(&updated).await?;
575 self.set_native_session_cookie(req, data.clone().into(), Some(false))
576 .await?;
577 } else {
578 self.write_cache_with_response(req, &data.clone().into(), false, None, None)
579 .await?;
580 }
581 Ok(SessionResolution {
582 data: Some(self.public_data(data)),
583 needs_refresh: None,
584 })
585 }
586
587 pub async fn write_cache(
589 &self,
590 req: &AuthRequest,
591 data: &SessionData,
592 dont_remember: bool,
593 ) -> AuthResult<()> {
594 self.write_cache_with_response(req, &data.clone().into(), dont_remember, None, None)
595 .await
596 }
597
598 async fn write_cache_with_response(
599 &self,
600 req: &AuthRequest,
601 data: &NativeSessionData,
602 dont_remember: bool,
603 response_headers: Option<&crate::Headers>,
604 transaction: Option<&dyn crate::store::AuthTransaction<S>>,
605 ) -> AuthResult<()> {
606 let signed = if let (Some(signer), Some(cache)) = (
607 &self.signer,
608 self.config
609 .session
610 .cookie_cache
611 .as_ref()
612 .filter(|cache| cache.enabled()),
613 ) {
614 let (payload, max_age) =
615 cookie_cache::payload(data, &self.config, cache, dont_remember).await?;
616 Some(
617 signer
618 .sign(
619 payload,
620 max_age,
621 SessionCookieContext {
622 request: req,
623 config: &self.config,
624 transaction,
625 },
626 )
627 .await?,
628 )
629 } else {
630 None
631 };
632 cookie_cache::write(
633 req,
634 data,
635 &self.config,
636 dont_remember,
637 signed,
638 self.capabilities.database,
639 response_headers,
640 )
641 .await
642 }
643
644 pub fn dont_remember(&self, req: &AuthRequest) -> bool {
646 get_cookie(req, &related_cookie_name(&self.config, "dont_remember"))
647 .and_then(|value| verify_cookie_value(&value, self.config.signing_secret()))
648 .is_some()
649 }
650
651 pub fn clear_cookies(&self, req: &AuthRequest) -> AuthResult<()> {
653 crate::utils::cookie_utils::delete_session_cookies(req, &self.config, false, None)
654 }
655
656 pub async fn delete_session(&self, token: &str) -> AuthResult<()> {
658 self.database.delete_session(token).await?;
659 Ok(())
660 }
661
662 pub async fn delete_user_sessions(&self, user_id: impl AsRef<str>) -> AuthResult<()> {
664 self.database.delete_user_sessions(user_id.as_ref()).await?;
665 Ok(())
666 }
667
668 pub async fn list_user_sessions(
670 &self,
671 user_id: impl AsRef<str>,
672 ) -> AuthResult<Vec<crate::wire::SessionView>> {
673 let sessions = self.database.get_user_sessions(user_id.as_ref()).await?;
674 let now = Utc::now();
675
676 let active_sessions = sessions
678 .into_iter()
679 .filter(|session| {
680 session.expires_at().milliseconds() > now.timestamp_millis() as f64
681 && session.active()
682 })
683 .collect();
684
685 Ok(active_sessions)
686 }
687
688 pub async fn list_user_session_views(
690 &self,
691 user_id: impl AsRef<str>,
692 ) -> AuthResult<Vec<SessionView>> {
693 let snapshots = self
694 .database
695 .get_user_session_snapshots(user_id.as_ref())
696 .await?;
697 let mut views = Vec::new();
698 for (session, cached) in snapshots.into_iter().filter(|(session, _)| {
699 session.expires_at().milliseconds() > Utc::now().timestamp_millis() as f64
700 && session.active()
701 }) {
702 let view = if let Some(mut view) = cached {
703 view.filter_returned_fields(&self.config.session);
704 view
705 } else {
706 SessionView::with_fields_for_adapter(
707 &session,
708 &self.config.session,
709 self.database.supports_native_json(),
710 )
711 .await?
712 };
713 views.push(view);
714 }
715 Ok(views)
716 }
717
718 pub async fn revoke_session(&self, token: &str) -> AuthResult<bool> {
720 let session_exists = self.get_session(token).await?.is_some();
722
723 if session_exists {
724 self.delete_session(token).await?;
725 Ok(true)
726 } else {
727 Ok(false)
728 }
729 }
730
731 pub async fn revoke_all_user_sessions(&self, user_id: impl AsRef<str>) -> AuthResult<usize> {
733 let user_id = user_id.as_ref();
735 let sessions = self.list_user_sessions(user_id).await?;
736 let count = sessions.len();
737
738 self.delete_user_sessions(user_id).await?;
739 Ok(count)
740 }
741
742 pub async fn revoke_other_user_sessions(
744 &self,
745 user_id: impl AsRef<str>,
746 current_token: &str,
747 ) -> AuthResult<usize> {
748 let sessions = self.list_user_sessions(user_id).await?;
749 let mut count = 0;
750
751 for session in sessions {
752 if session.token() != current_token {
753 self.delete_session(session.token()).await?;
754 count += 1;
755 }
756 }
757
758 Ok(count)
759 }
760
761 pub async fn cleanup_expired_sessions(&self) -> AuthResult<usize> {
763 let count = self.database.delete_expired_sessions().await?;
764 Ok(count)
765 }
766
767 pub fn is_session_fresh(&self, session: &impl AuthSession) -> bool {
774 match self.config.session.fresh_age {
775 Some(fresh_age) => {
776 session.created_at().milliseconds() + fresh_age.num_milliseconds() as f64
777 > Utc::now().timestamp_millis() as f64
778 }
779 None => false,
780 }
781 }
782
783 pub fn validate_token_format(&self, token: &str) -> bool {
785 token.starts_with("session_") && token.len() > 40
786 }
787
788 pub fn extract_session_token(&self, req: &AuthRequest) -> Option<String> {
790 if let Some(bearer) = &self.config.session.bearer
791 && let Some(header) = req.headers.get("authorization")
792 && header
793 .get(..7)
794 .is_some_and(|scheme| scheme.eq_ignore_ascii_case("bearer "))
795 {
796 let token = header.get(7..)?.trim();
797 if !token.is_empty() {
798 if token.contains('.') {
799 if let Some(value) = verify_cookie_value(token, self.config.signing_secret()) {
800 return Some(value);
801 }
802 } else if !bearer.require_signature {
803 return Some(token.to_string());
804 }
805 }
806 }
807 get_cookie(
808 req,
809 &self
810 .config
811 .auth_cookie("session_token", Default::default())
812 .name,
813 )
814 .and_then(|value| verify_cookie_value(&value, self.config.signing_secret()))
815 }
816}
817
818fn query_flag(req: &AuthRequest, name: &str) -> AuthResult<bool> {
819 req.query
820 .as_ref()
821 .and_then(|query| query.get(name))
822 .map(|value| crate::FieldValue::from_json(value.clone()).map(|value| value.is_truthy()))
823 .transpose()
824 .map(|value| value.unwrap_or(false))
825}
826
827fn relationship_array_error() -> AuthError {
828 AuthError::internal("A User relationship array cannot authenticate a typed User")
829}
830
831fn failed_session_update() -> AuthError {
832 AuthError::Upstream {
833 status: 401,
834 code: "FAILED_TO_GET_SESSION",
835 message: "Failed to get session",
836 }
837}
838
839#[cfg(test)]
840mod tests {
841 use super::*;
842 use crate::entity::AuthSession;
843 use crate::test_store::{BundledSchema, test_config, test_database};
844 use crate::types::AuthRequest;
845 use crate::types::HttpMethod;
846 use crate::utils::cookie_utils::sign_cookie_value;
847 use crate::wire::SessionView;
848 use chrono::Duration;
849
850 fn test_manager() -> SessionManager<BundledSchema> {
851 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
852 SessionManager::new(test_config(), runtime.block_on(test_database()))
853 }
854
855 #[test]
859 fn valid_token_format() {
860 let mgr = test_manager();
861 let token = "session_abcdefghijklmnopqrstuvwxyz1234567890";
862 assert!(mgr.validate_token_format(token));
863 }
864
865 #[test]
867 fn invalid_token_no_prefix() {
868 let mgr = test_manager();
869 assert!(!mgr.validate_token_format("abcdefghijklmnopqrstuvwxyz1234567890"));
870 }
871
872 #[test]
874 fn invalid_token_too_short() {
875 let mgr = test_manager();
876 assert!(!mgr.validate_token_format("session_short"));
877 }
878
879 #[test]
883 fn extract_from_bearer() {
884 let mgr = test_manager();
885 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
886 let _ = req
887 .headers
888 .insert("authorization".into(), "Bearer my-token".into());
889 assert_eq!(mgr.extract_session_token(&req), Some("my-token".into()));
890 }
891
892 #[test]
894 fn extract_from_cookie() {
895 let mgr = test_manager();
896 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
897 let _ = req.headers.insert(
898 "cookie".into(),
899 format!(
900 "better-auth.session_token={}; other=val",
901 sign_cookie_value("tok123", &mgr.config.secret)
902 ),
903 );
904 assert_eq!(mgr.extract_session_token(&req), Some("tok123".into()));
905 }
906
907 #[test]
909 fn extract_bearer_takes_precedence_over_cookie() {
910 let mgr = test_manager();
911 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
912 let _ = req
913 .headers
914 .insert("authorization".into(), "Bearer bearer-tok".into());
915 let _ = req.headers.insert(
916 "cookie".into(),
917 format!(
918 "better-auth.session_token={}",
919 sign_cookie_value("cookie-tok", &mgr.config.secret)
920 ),
921 );
922 assert_eq!(mgr.extract_session_token(&req), Some("bearer-tok".into()));
923 }
924
925 #[test]
927 fn extract_returns_none_without_auth() {
928 let mgr = test_manager();
929 let req = AuthRequest::new(HttpMethod::Get, "/test");
930 assert_eq!(mgr.extract_session_token(&req), None);
931 }
932
933 #[test]
935 fn extract_skips_empty_cookie_value() {
936 let mgr = test_manager();
937 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
938 let _ = req
939 .headers
940 .insert("cookie".into(), "better-auth.session_token=".into());
941 assert_eq!(mgr.extract_session_token(&req), None);
942 }
943
944 #[test]
948 fn session_fresh_when_within_window() {
949 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
950 config.session.fresh_age = Some(Duration::minutes(10));
951 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
952 let mgr = SessionManager::new(Arc::new(config), runtime.block_on(test_database()));
953
954 let session = SessionView {
956 visible_fields: None,
957 id: "s1".into(),
958 expires_at: (Utc::now() + Duration::hours(1)).into(),
959 token: "tok".into(),
960 created_at: Utc::now().into(),
961 updated_at: Utc::now().into(),
962 ip_address: None,
963 user_agent: None,
964 user_id: "u1".into(),
965 impersonated_by: None,
966 active_organization_id: None,
967 active_team_id: None,
968 active: true,
969 additional_fields: Default::default(),
970 };
971 assert!(mgr.is_session_fresh(&session));
972 }
973
974 #[test]
976 fn session_not_fresh_when_old() {
977 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
978 config.session.fresh_age = Some(Duration::minutes(10));
979 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
980 let mgr = SessionManager::new(Arc::new(config), runtime.block_on(test_database()));
981
982 let session = SessionView {
983 visible_fields: None,
984 id: "s1".into(),
985 expires_at: (Utc::now() + Duration::hours(1)).into(),
986 token: "tok".into(),
987 created_at: (Utc::now() - Duration::minutes(20)).into(),
988 updated_at: Utc::now().into(),
989 ip_address: None,
990 user_agent: None,
991 user_id: "u1".into(),
992 impersonated_by: None,
993 active_organization_id: None,
994 active_team_id: None,
995 active: true,
996 additional_fields: Default::default(),
997 };
998 assert!(!mgr.is_session_fresh(&session));
999 }
1000
1001 #[test]
1003 fn session_never_fresh_when_no_fresh_age() {
1004 let mgr = test_manager(); let session = SessionView {
1006 visible_fields: None,
1007 id: "s1".into(),
1008 expires_at: (Utc::now() + Duration::hours(1)).into(),
1009 token: "tok".into(),
1010 created_at: Utc::now().into(),
1011 updated_at: Utc::now().into(),
1012 ip_address: None,
1013 user_agent: None,
1014 user_id: "u1".into(),
1015 impersonated_by: None,
1016 active_organization_id: None,
1017 active_team_id: None,
1018 active: true,
1019 additional_fields: Default::default(),
1020 };
1021 assert!(!mgr.is_session_fresh(&session));
1022 }
1023
1024 #[tokio::test]
1028 async fn create_and_get_session() {
1029 let db = test_database().await;
1030 let mgr = SessionManager::new(test_config(), db.clone());
1031
1032 let user = db
1034 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1035 .await
1036 .unwrap();
1037
1038 let session = mgr.create_session(&user, None, None).await.unwrap();
1039 let token = session.token().to_string();
1040
1041 let retrieved = mgr.get_session(&token).await.unwrap();
1042 assert!(retrieved.is_some());
1043 }
1044
1045 #[tokio::test]
1047 async fn refresh_returns_the_persisted_expiry() {
1048 let db = test_database().await;
1049 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
1050 config.session.update_age = Some(Duration::zero());
1052 let mgr = SessionManager::new(Arc::new(config), db.clone());
1053
1054 let user = db
1055 .create_user(crate::types::CreateUser::new().with_email("refresh@test.com"))
1056 .await
1057 .unwrap();
1058 let session = mgr.create_session(&user, None, None).await.unwrap();
1059 let token = session.token().to_string();
1060
1061 let stale = session.expires_at().to_datetime().unwrap().unwrap() - Duration::minutes(30);
1063 let _ = db.update_session_expiry(&token, stale).await.unwrap();
1064
1065 let returned = mgr
1066 .get_session(&token)
1067 .await
1068 .unwrap()
1069 .expect("session should still be live");
1070 let stored = db
1071 .get_session(&token)
1072 .await
1073 .unwrap()
1074 .expect("session should still be stored");
1075
1076 assert!(
1077 returned.expires_at().milliseconds() > stale.timestamp_millis() as f64,
1078 "refresh should have extended the expiry"
1079 );
1080 assert_eq!(
1081 returned.expires_at(),
1082 stored.expires_at(),
1083 "returned session must reflect the persisted expiry, not the pre-refresh value"
1084 );
1085 }
1086
1087 #[tokio::test]
1089 async fn create_session_without_metadata_uses_empty_strings() {
1090 let db = test_database().await;
1091 let mgr = SessionManager::new(test_config(), db.clone());
1092
1093 let user = db
1094 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1095 .await
1096 .unwrap();
1097
1098 let session = mgr.create_session(&user, None, None).await.unwrap();
1099 assert_eq!(session.ip_address.as_deref(), Some(""));
1100 assert_eq!(session.user_agent.as_deref(), Some(""));
1101 }
1102
1103 #[tokio::test]
1105 async fn delete_session_removes_it() {
1106 let db = test_database().await;
1107 let mgr = SessionManager::new(test_config(), db.clone());
1108
1109 let user = db
1110 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1111 .await
1112 .unwrap();
1113
1114 let session = mgr.create_session(&user, None, None).await.unwrap();
1115 let token = session.token().to_string();
1116
1117 mgr.delete_session(&token).await.unwrap();
1118 let retrieved = mgr.get_session(&token).await.unwrap();
1119 assert!(retrieved.is_none());
1120 }
1121
1122 #[tokio::test]
1124 async fn revoke_session_returns_true_when_found() {
1125 let db = test_database().await;
1126 let mgr = SessionManager::new(test_config(), db.clone());
1127
1128 let user = db
1129 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1130 .await
1131 .unwrap();
1132
1133 let session = mgr.create_session(&user, None, None).await.unwrap();
1134 let result = mgr.revoke_session(session.token()).await.unwrap();
1135 assert!(result);
1136 }
1137
1138 #[tokio::test]
1140 async fn revoke_session_returns_false_when_not_found() {
1141 let mgr = SessionManager::new(test_config(), test_database().await);
1142 let result = mgr.revoke_session("nonexistent-token").await.unwrap();
1143 assert!(!result);
1144 }
1145
1146 #[tokio::test]
1148 async fn list_user_sessions_excludes_expired() {
1149 let db = test_database().await;
1150 let mgr = SessionManager::new(test_config(), db.clone());
1151
1152 let user = db
1153 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1154 .await
1155 .unwrap();
1156
1157 let _ = mgr.create_session(&user, None, None).await.unwrap();
1159 let _ = mgr.create_session(&user, None, None).await.unwrap();
1160
1161 let sessions = mgr
1162 .list_user_sessions(user.id().typed().unwrap())
1163 .await
1164 .unwrap();
1165 assert_eq!(sessions.len(), 2);
1166 }
1167
1168 #[tokio::test]
1170 async fn revoke_all_user_sessions() {
1171 let db = test_database().await;
1172 let mgr = SessionManager::new(test_config(), db.clone());
1173
1174 let user = db
1175 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1176 .await
1177 .unwrap();
1178
1179 let _ = mgr.create_session(&user, None, None).await.unwrap();
1180 let _ = mgr.create_session(&user, None, None).await.unwrap();
1181
1182 let count = mgr
1183 .revoke_all_user_sessions(user.id().typed().unwrap())
1184 .await
1185 .unwrap();
1186 assert_eq!(count, 2);
1187
1188 let sessions = mgr
1189 .list_user_sessions(user.id().typed().unwrap())
1190 .await
1191 .unwrap();
1192 assert!(sessions.is_empty());
1193 }
1194
1195 #[tokio::test]
1197 async fn revoke_other_sessions_keeps_current() {
1198 let db = test_database().await;
1199 let mgr = SessionManager::new(test_config(), db.clone());
1200
1201 let user = db
1202 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1203 .await
1204 .unwrap();
1205
1206 let current = mgr.create_session(&user, None, None).await.unwrap();
1207 let _ = mgr.create_session(&user, None, None).await.unwrap();
1208 let _ = mgr.create_session(&user, None, None).await.unwrap();
1209
1210 let count = mgr
1211 .revoke_other_user_sessions(user.id().typed().unwrap(), current.token())
1212 .await
1213 .unwrap();
1214 assert_eq!(count, 2);
1215
1216 let remaining = mgr
1217 .list_user_sessions(user.id().typed().unwrap())
1218 .await
1219 .unwrap();
1220 assert_eq!(remaining.len(), 1);
1221 assert_eq!(remaining[0].token(), current.token());
1222 }
1223}