1pub mod cookie_cache;
2mod request;
3use crate::config::AuthConfig;
4use crate::entity::{AuthSession, AuthUser};
5use crate::error::AuthResult;
6use crate::schema::AuthSchema;
7use crate::store::AuthStore;
8use crate::types::CreateSession;
9use chrono::Utc;
10pub use request::SessionRequest;
11use std::sync::Arc;
12
13#[derive(Clone, Copy, Debug)]
17pub struct SessionRefreshSuppressed;
18
19#[derive(Debug, Clone, Copy)]
21pub struct SessionReadOptions {
22 pub allow_refresh: bool,
23 pub cleanup_expired: bool,
24}
25
26impl Default for SessionReadOptions {
27 fn default() -> Self {
28 Self {
29 allow_refresh: true,
30 cleanup_expired: true,
31 }
32 }
33}
34
35pub struct SessionRead<T> {
37 pub session: Option<T>,
38 pub needs_refresh: bool,
39 pub refreshed: bool,
40}
41
42pub struct SessionManager<S: AuthSchema> {
44 config: Arc<AuthConfig>,
45 database: Arc<dyn AuthStore<S>>,
46}
47
48impl<S: AuthSchema> Clone for SessionManager<S> {
49 fn clone(&self) -> Self {
50 Self {
51 config: Arc::clone(&self.config),
52 database: Arc::clone(&self.database),
53 }
54 }
55}
56
57impl<S: AuthSchema> SessionManager<S> {
58 #[must_use]
59 pub fn new(config: Arc<AuthConfig>, database: Arc<dyn AuthStore<S>>) -> Self {
60 Self { config, database }
61 }
62
63 pub async fn create_session(
69 &self,
70 user: &impl AuthUser,
71 ip_address: Option<String>,
72 user_agent: Option<String>,
73 ) -> AuthResult<S::Session> {
74 self.database
75 .create_session(self.new_session(user, ip_address, user_agent))
76 .await
77 }
78
79 pub async fn create_session_record(
84 &self,
85 user: &impl AuthUser,
86 ip_address: Option<String>,
87 user_agent: Option<String>,
88 ) -> AuthResult<crate::AdapterRecord<S::Session>> {
89 self.database
90 .create_session_record(self.new_session(user, ip_address, user_agent))
91 .await
92 }
93
94 fn new_session(
95 &self,
96 user: &impl AuthUser,
97 ip_address: Option<String>,
98 user_agent: Option<String>,
99 ) -> CreateSession {
100 CreateSession {
101 additional_fields: crate::field_policy::FieldValues::default(),
102 token: None,
103 active_team_id: None,
104 user_id: user.id().to_string(),
105 expires_at: Utc::now() + self.config.session.expires_in,
106 ip_address,
107 user_agent,
108 impersonated_by: None,
109 active_organization_id: None,
110 }
111 }
112
113 pub async fn get_session(&self, token: &str) -> AuthResult<Option<S::Session>> {
119 Ok(self
120 .read_session(token, SessionReadOptions::default())
121 .await?
122 .session)
123 }
124
125 pub async fn read_session(
132 &self,
133 token: &str,
134 options: SessionReadOptions,
135 ) -> AuthResult<SessionRead<S::Session>> {
136 let Some(session) = self.database.get_session(token).await? else {
137 return Ok(SessionRead {
138 session: None,
139 needs_refresh: false,
140 refreshed: false,
141 });
142 };
143 self.read_loaded_session(session, options).await
144 }
145
146 pub async fn read_loaded_session(
155 &self,
156 session: S::Session,
157 options: SessionReadOptions,
158 ) -> AuthResult<SessionRead<S::Session>> {
159 self.read_loaded(session, options, |token, expires_at| async move {
160 self.database.refresh_session(&token, expires_at).await
161 })
162 .await
163 }
164
165 pub async fn read_loaded_session_record(
168 &self,
169 session: crate::AdapterRecord<S::Session>,
170 options: SessionReadOptions,
171 ) -> AuthResult<SessionRead<crate::AdapterRecord<S::Session>>> {
172 self.read_loaded(session, options, |token, expires_at| async move {
173 self.database
174 .refresh_session_record(&token, expires_at)
175 .await
176 })
177 .await
178 }
179
180 async fn read_loaded<T, F, Fut>(
181 &self,
182 session: T,
183 options: SessionReadOptions,
184 refresh: F,
185 ) -> AuthResult<SessionRead<T>>
186 where
187 T: AuthSession,
188 F: FnOnce(String, chrono::DateTime<chrono::Utc>) -> Fut,
189 Fut: std::future::Future<Output = AuthResult<Option<T>>>,
190 {
191 let token = session.token();
192 let now = Utc::now();
193 if session.expires_at() < now || !session.active() {
194 if options.cleanup_expired {
195 self.database.delete_session(token).await?;
196 }
197 return Ok(SessionRead {
198 session: None,
199 needs_refresh: false,
200 refreshed: false,
201 });
202 }
203 let needs_refresh = !self.config.session.disable_session_refresh
204 && self.config.session.update_age.is_none_or(|age| {
205 session.expires_at() - self.config.session.expires_in + age <= now
206 });
207 if needs_refresh && options.allow_refresh {
208 let refreshed_session =
209 refresh(token.to_owned(), now + self.config.session.expires_in).await?;
210 let refreshed = refreshed_session.is_some();
211 return Ok(SessionRead {
212 session: refreshed_session,
213 needs_refresh,
214 refreshed,
215 });
216 }
217 Ok(SessionRead {
218 session: Some(session),
219 needs_refresh,
220 refreshed: false,
221 })
222 }
223
224 #[must_use]
228 pub fn request_disables_refresh(&self, request: &impl SessionRequest) -> bool {
229 if request.session_query_truthy("disableRefresh") {
230 return true;
231 }
232 self.has_dont_remember_cookie(request)
233 }
234
235 #[must_use]
237 pub fn has_dont_remember_cookie(&self, request: &impl SessionRequest) -> bool {
238 let name = crate::utils::cookie_utils::related_cookie_name(&self.config, "dont_remember");
239 request
240 .session_headers()
241 .get("cookie")
242 .is_some_and(|header| {
243 cookie::Cookie::split_parse(header)
244 .flatten()
245 .find(|cookie| cookie.name() == name)
246 .and_then(|cookie| {
247 crate::utils::cookie_utils::verify_cookie_value(
248 cookie.value(),
249 self.config.current_secret(),
250 )
251 })
252 .is_some_and(|value| !value.is_empty())
253 })
254 }
255
256 pub async fn delete_session(&self, token: &str) -> AuthResult<()> {
262 self.database.delete_session(token).await?;
263 Ok(())
264 }
265
266 pub async fn delete_user_sessions(&self, user_id: impl AsRef<str>) -> AuthResult<()> {
272 self.database.delete_user_sessions(user_id.as_ref()).await?;
273 Ok(())
274 }
275
276 pub async fn list_user_sessions(
282 &self,
283 user_id: impl AsRef<str>,
284 ) -> AuthResult<Vec<S::Session>> {
285 let sessions = self.database.get_user_sessions(user_id.as_ref()).await?;
286 let now = Utc::now();
287
288 let active_sessions = sessions
290 .into_iter()
291 .filter(|session| session.expires_at() > now && session.active())
292 .collect();
293
294 Ok(active_sessions)
295 }
296
297 pub async fn revoke_session(&self, token: &str) -> AuthResult<bool> {
303 let session_exists = self.get_session(token).await?.is_some();
305
306 if session_exists {
307 self.delete_session(token).await?;
308 Ok(true)
309 } else {
310 Ok(false)
311 }
312 }
313
314 pub async fn revoke_all_user_sessions(&self, user_id: impl AsRef<str>) -> AuthResult<usize> {
320 let user_id = user_id.as_ref();
322 let sessions = self.list_user_sessions(user_id).await?;
323 let count = sessions.len();
324
325 self.delete_user_sessions(user_id).await?;
326 Ok(count)
327 }
328
329 pub async fn revoke_other_user_sessions(
335 &self,
336 user_id: impl AsRef<str>,
337 current_token: &str,
338 ) -> AuthResult<usize> {
339 let sessions = self.list_user_sessions(user_id).await?;
340 let mut count = 0;
341
342 for session in sessions {
343 if session.token() != current_token {
344 self.delete_session(session.token()).await?;
345 count += 1;
346 }
347 }
348
349 Ok(count)
350 }
351
352 pub async fn cleanup_expired_sessions(&self) -> AuthResult<usize> {
358 let count = self.database.delete_expired_sessions().await?;
359 Ok(count)
360 }
361
362 pub fn is_session_fresh(&self, session: &impl AuthSession) -> bool {
368 match self.config.session.fresh_age {
369 Some(fresh_age) if fresh_age != chrono::Duration::zero() => {
370 session.created_at() + fresh_age > Utc::now()
371 }
372 _ => true,
373 }
374 }
375
376 #[must_use]
378 pub fn validate_token_format(&self, token: &str) -> bool {
379 token.len() == 32 && token.bytes().all(|byte| byte.is_ascii_alphanumeric())
380 }
381
382 #[must_use]
387 pub fn extract_session_token(&self, req: &impl SessionRequest) -> Option<String> {
388 let header = req.session_headers().get("cookie")?;
389 header
390 .split(';')
391 .find_map(|pair| {
392 let (name, value) = pair.split_once('=')?;
393 (crate::utils::javascript::trim(name) == self.config.session.cookie_name)
394 .then(|| crate::utils::javascript::trim(value))
395 })
396 .map(|value| {
397 if value.starts_with('"') {
400 value.get(1..value.len().saturating_sub(1)).unwrap_or("")
401 } else {
402 value
403 }
404 })
405 .and_then(|value| {
406 crate::utils::cookie_utils::verify_cookie_value(value, self.config.current_secret())
407 })
408 .filter(|token| !token.is_empty())
409 }
410}
411
412impl<T> std::fmt::Debug for SessionRead<T> {
413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 f.debug_struct("SessionRead").finish_non_exhaustive()
415 }
416}
417
418impl<S: AuthSchema> std::fmt::Debug for SessionManager<S> {
419 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420 f.debug_struct("SessionManager").finish_non_exhaustive()
421 }
422}
423
424#[cfg(test)]
426mod tests {
427 use super::*;
428 use crate::entity::AuthSession;
429 use crate::test_store::{BundledSchema, test_config, test_database};
430 use crate::types::AuthRequest;
431 use crate::types::HttpMethod;
432 use crate::wire::SessionView;
433 use chrono::Duration;
434
435 fn test_manager() -> SessionManager<BundledSchema> {
436 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
437 SessionManager::new(test_config(), runtime.block_on(test_database()))
438 }
439
440 #[test]
444 fn valid_token_format() {
445 let mgr = test_manager();
446 let token = "abcdefghijklmnopqrstuvwxyz123456";
447 assert!(mgr.validate_token_format(token));
448 }
449
450 #[test]
452 fn invalid_token_non_alphanumeric() {
453 let mgr = test_manager();
454 assert!(!mgr.validate_token_format("abcdefghijklmnopqrstuvwxy_123456"));
455 }
456
457 #[test]
459 fn invalid_token_too_short() {
460 let mgr = test_manager();
461 assert!(!mgr.validate_token_format("session_short"));
462 }
463
464 #[test]
469 fn extract_rejects_bare_bearer() {
470 let mgr = test_manager();
471 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
472 drop(
473 req.headers
474 .insert("authorization".into(), "Bearer my-token".into()),
475 );
476 assert_eq!(mgr.extract_session_token(&req), None);
477 }
478
479 #[test]
480 fn extract_cookie_checks_signature_and_first_duplicate() {
481 let mgr = test_manager();
482 let signed = crate::utils::cookie_utils::sign_cookie_value("tok123", &mgr.config.secret);
483 let foreign =
484 crate::utils::cookie_utils::sign_cookie_value("tok123", "another-server-secret");
485 for (cookie, expected) in [
486 (
487 format!("better-auth.session_token={signed}; other=val"),
488 Some("tok123"),
489 ),
490 ("better-auth.session_token=tok123".to_owned(), None),
491 (format!("better-auth.session_token={foreign}"), None),
492 (
493 format!("better-auth.session_token={signed}; better-auth.session_token=invalid"),
494 Some("tok123"),
495 ),
496 (
497 format!("better-auth.session_token=invalid; better-auth.session_token={signed}"),
498 None,
499 ),
500 ] {
501 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
502 drop(req.headers.insert("cookie".into(), cookie));
503 assert_eq!(mgr.extract_session_token(&req).as_deref(), expected);
504 }
505 }
506
507 #[test]
508 fn extract_ignores_bearer_when_signed_cookie_exists() {
509 let mgr = test_manager();
510 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
511 drop(
512 req.headers
513 .insert("authorization".into(), "Bearer bearer-tok".into()),
514 );
515 let signed =
516 crate::utils::cookie_utils::sign_cookie_value("cookie-tok", &mgr.config.secret);
517 drop(req.headers.insert(
518 "cookie".into(),
519 format!("better-auth.session_token={signed}"),
520 ));
521 assert_eq!(mgr.extract_session_token(&req), Some("cookie-tok".into()));
522 }
523
524 #[test]
526 fn extract_returns_none_without_auth() {
527 let mgr = test_manager();
528 let req = AuthRequest::new(HttpMethod::Get, "/test");
529 assert_eq!(mgr.extract_session_token(&req), None);
530 }
531
532 #[test]
534 fn extract_skips_empty_cookie_value() {
535 let mgr = test_manager();
536 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
537 drop(
538 req.headers
539 .insert("cookie".into(), "better-auth.session_token=".into()),
540 );
541 assert_eq!(mgr.extract_session_token(&req), None);
542 }
543
544 #[test]
548 fn session_fresh_when_within_window() {
549 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
550 config.session.fresh_age = Some(Duration::minutes(10));
551 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
552 let mgr = SessionManager::new(Arc::new(config), runtime.block_on(test_database()));
553
554 let session = SessionView {
556 omitted_fields: std::collections::BTreeSet::default(),
557 active_team_id: None,
558 extension_fields: std::collections::BTreeMap::default(),
559 id: "s1".into(),
560 expires_at: Utc::now() + Duration::hours(1),
561 token: "tok".into(),
562 created_at: Utc::now(),
563 updated_at: Utc::now(),
564 ip_address: None,
565 user_agent: None,
566 user_id: "u1".into(),
567 impersonated_by: None,
568 active_organization_id: None,
569 active: true,
570 };
571 assert!(mgr.is_session_fresh(&session));
572 }
573
574 #[test]
576 fn session_not_fresh_when_old() {
577 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
578 config.session.fresh_age = Some(Duration::minutes(10));
579 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
580 let mgr = SessionManager::new(Arc::new(config), runtime.block_on(test_database()));
581
582 let session = SessionView {
583 omitted_fields: std::collections::BTreeSet::default(),
584 active_team_id: None,
585 extension_fields: std::collections::BTreeMap::default(),
586 id: "s1".into(),
587 expires_at: Utc::now() + Duration::hours(1),
588 token: "tok".into(),
589 created_at: Utc::now() - Duration::minutes(20),
590 updated_at: Utc::now(),
591 ip_address: None,
592 user_agent: None,
593 user_id: "u1".into(),
594 impersonated_by: None,
595 active_organization_id: None,
596 active: true,
597 };
598 assert!(!mgr.is_session_fresh(&session));
599 }
600
601 #[test]
603 fn disabled_freshness_allows_an_old_session() {
604 let mut config = (*test_config()).clone();
605 config.session.fresh_age = None;
606 let mgr = SessionManager::new(Arc::new(config), test_manager().database);
607 let session = SessionView {
608 omitted_fields: std::collections::BTreeSet::default(),
609 active_team_id: None,
610 extension_fields: std::collections::BTreeMap::default(),
611 id: "s1".into(),
612 expires_at: Utc::now() + Duration::hours(1),
613 token: "tok".into(),
614 created_at: Utc::now() - Duration::days(30),
615 updated_at: Utc::now(),
616 ip_address: None,
617 user_agent: None,
618 user_id: "u1".into(),
619 impersonated_by: None,
620 active_organization_id: None,
621 active: true,
622 };
623 assert!(mgr.is_session_fresh(&session));
624 }
625
626 #[tokio::test]
630 async fn create_and_get_session() {
631 let db = test_database().await;
632 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
633
634 let user = db
636 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
637 .await
638 .unwrap();
639
640 let session = mgr.create_session(&user, None, None).await.unwrap();
641 let token = session.token().to_owned();
642
643 let retrieved = mgr.get_session(&token).await.unwrap();
644 assert!(retrieved.is_some());
645 }
646
647 #[tokio::test]
649 async fn refresh_returns_the_persisted_expiry() {
650 let db = test_database().await;
651 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
652 config.session.update_age = None;
654 let mgr = SessionManager::new(Arc::new(config), Arc::clone(&db));
655
656 let user = db
657 .create_user(crate::types::CreateUser::new().with_email("refresh@test.com"))
658 .await
659 .unwrap();
660 let session = mgr.create_session(&user, None, None).await.unwrap();
661 let token = session.token().to_owned();
662
663 let stale = session.expires_at() - Duration::minutes(30);
665 db.update_session_expiry(&token, stale).await.unwrap();
666
667 let returned = mgr
668 .get_session(&token)
669 .await
670 .unwrap()
671 .expect("session should still be live");
672 let stored = db
673 .get_session(&token)
674 .await
675 .unwrap()
676 .expect("session should still be stored");
677
678 assert!(
679 returned.expires_at() > stale,
680 "refresh should have extended the expiry"
681 );
682 assert_eq!(
683 returned.expires_at(),
684 stored.expires_at(),
685 "returned session must reflect the persisted expiry, not the pre-refresh value"
686 );
687 }
688
689 #[test]
690 fn refresh_preferences_verify_the_first_cookie_and_use_javascript_truthiness() {
691 let manager = test_manager();
692 let name =
693 crate::utils::cookie_utils::related_cookie_name(&manager.config, "dont_remember");
694 let valid = crate::utils::cookie_utils::sign_cookie_value("true", &manager.config.secret);
695 let empty = crate::utils::cookie_utils::sign_cookie_value("", &manager.config.secret);
696 let wrong = crate::utils::cookie_utils::sign_cookie_value("true", "foreign-secret");
697 for (header, expected) in [
698 (format!("{name}={valid}"), true),
699 (format!("{name}={empty}"), false),
700 (format!("{name}={wrong}"), false),
701 (format!("{name}=invalid; {name}={valid}"), false),
702 (format!("{name}={valid}; {name}=invalid"), true),
703 ] {
704 let mut request = AuthRequest::new(HttpMethod::Get, "/get-session");
705 drop(request.headers.insert("cookie".into(), header));
706 assert_eq!(manager.request_disables_refresh(&request), expected);
707 }
708 for (value, expected) in [("", false), ("false", true), ("0", true), ("true", true)] {
709 let mut request = AuthRequest::new(HttpMethod::Get, "/get-session");
710 drop(request.query.insert("disableRefresh".into(), value.into()));
711 assert_eq!(manager.request_disables_refresh(&request), expected);
712 }
713 }
714
715 #[tokio::test]
716 async fn recent_sessions_refresh_at_the_expiry_based_half_second_boundary() {
717 let db = test_database().await;
718 let config = test_config();
719 let manager = SessionManager::new(Arc::clone(&config), Arc::clone(&db));
720 let user = db
721 .create_user(crate::types::CreateUser::new().with_email("half-second@test.com"))
722 .await
723 .unwrap();
724 for difference in [Duration::milliseconds(500), Duration::milliseconds(-500)] {
725 let session = manager.create_session(&user, None, None).await.unwrap();
726 let expiry = Utc::now() + config.session.expires_in
727 - config.session.update_age.unwrap()
728 + difference;
729 db.update_session_expiry(session.token(), expiry)
730 .await
731 .unwrap();
732 let returned = manager.get_session(session.token()).await.unwrap().unwrap();
733 let stored = db.get_session(session.token()).await.unwrap().unwrap();
734 assert_eq!(returned.expires_at(), stored.expires_at());
735 assert_eq!(returned.token(), session.token());
736 if difference > Duration::zero() {
737 assert_eq!(stored.expires_at(), expiry);
738 } else {
739 assert!(stored.expires_at() > expiry + Duration::hours(23));
740 }
741 }
742 }
743
744 #[tokio::test]
746 async fn create_session_without_metadata_uses_empty_strings() {
747 let db = test_database().await;
748 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
749
750 let user = db
751 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
752 .await
753 .unwrap();
754
755 let session = mgr.create_session(&user, None, None).await.unwrap();
756 assert_eq!(session.ip_address.as_deref(), Some(""));
757 assert_eq!(session.user_agent.as_deref(), Some(""));
758 }
759
760 #[tokio::test]
762 async fn delete_session_removes_it() {
763 let db = test_database().await;
764 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
765
766 let user = db
767 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
768 .await
769 .unwrap();
770
771 let session = mgr.create_session(&user, None, None).await.unwrap();
772 let token = session.token().to_owned();
773
774 mgr.delete_session(&token).await.unwrap();
775 let retrieved = mgr.get_session(&token).await.unwrap();
776 assert!(retrieved.is_none());
777 }
778
779 #[tokio::test]
781 async fn revoke_session_returns_true_when_found() {
782 let db = test_database().await;
783 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
784
785 let user = db
786 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
787 .await
788 .unwrap();
789
790 let session = mgr.create_session(&user, None, None).await.unwrap();
791 let result = mgr.revoke_session(session.token()).await.unwrap();
792 assert!(result);
793 }
794
795 #[tokio::test]
797 async fn revoke_session_returns_false_when_not_found() {
798 let mgr = SessionManager::new(test_config(), test_database().await);
799 let result = mgr.revoke_session("nonexistent-token").await.unwrap();
800 assert!(!result);
801 }
802
803 #[tokio::test]
805 async fn list_user_sessions_excludes_expired() {
806 let db = test_database().await;
807 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
808
809 let user = db
810 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
811 .await
812 .unwrap();
813
814 drop(mgr.create_session(&user, None, None).await.unwrap());
816 drop(mgr.create_session(&user, None, None).await.unwrap());
817
818 let sessions = mgr.list_user_sessions(user.id()).await.unwrap();
819 assert_eq!(sessions.len(), 2);
820 }
821
822 #[tokio::test]
824 async fn revoke_all_user_sessions() {
825 let db = test_database().await;
826 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
827
828 let user = db
829 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
830 .await
831 .unwrap();
832
833 drop(mgr.create_session(&user, None, None).await.unwrap());
834 drop(mgr.create_session(&user, None, None).await.unwrap());
835
836 let count = mgr.revoke_all_user_sessions(user.id()).await.unwrap();
837 assert_eq!(count, 2);
838
839 let sessions = mgr.list_user_sessions(user.id()).await.unwrap();
840 assert_eq!(sessions, Vec::<SessionView>::new());
841 }
842
843 #[tokio::test]
845 async fn revoke_other_sessions_keeps_current() {
846 let db = test_database().await;
847 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
848
849 let user = db
850 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
851 .await
852 .unwrap();
853
854 let current = mgr.create_session(&user, None, None).await.unwrap();
855 drop(mgr.create_session(&user, None, None).await.unwrap());
856 drop(mgr.create_session(&user, None, None).await.unwrap());
857
858 let count = mgr
859 .revoke_other_user_sessions(user.id(), current.token())
860 .await
861 .unwrap();
862 assert_eq!(count, 2);
863
864 let remaining = mgr.list_user_sessions(user.id()).await.unwrap();
865 assert_eq!(remaining.len(), 1);
866 assert_eq!(
867 (*(remaining)
868 .first()
869 .expect("fixture contains the requested index"))
870 .token(),
871 current.token()
872 );
873 }
874}
875