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;
9use crate::types::CreateSession;
10
11pub struct SessionManager<S: AuthSchema> {
13 config: Arc<AuthConfig>,
14 database: Arc<dyn AuthStore<S>>,
15}
16
17impl<S: AuthSchema> Clone for SessionManager<S> {
18 fn clone(&self) -> Self {
19 Self {
20 config: self.config.clone(),
21 database: self.database.clone(),
22 }
23 }
24}
25
26impl<S: AuthSchema> SessionManager<S> {
27 pub fn new(config: Arc<AuthConfig>, database: Arc<dyn AuthStore<S>>) -> Self {
28 Self { config, database }
29 }
30
31 pub async fn create_session(
33 &self,
34 user: &impl AuthUser,
35 ip_address: Option<String>,
36 user_agent: Option<String>,
37 ) -> AuthResult<S::Session> {
38 let expires_at = Utc::now() + self.config.session.expires_in;
39
40 let create_session = CreateSession {
41 user_id: user.id().to_string(),
42 expires_at,
43 ip_address,
44 user_agent,
45 impersonated_by: None,
46 active_organization_id: None,
47 };
48
49 let session = self.database.create_session(create_session).await?;
50 Ok(session)
51 }
52
53 pub async fn get_session(&self, token: &str) -> AuthResult<Option<S::Session>> {
55 let mut session = self.database.get_session(token).await?;
56
57 let should_refresh = if let Some(ref s) = session {
59 let now = Utc::now();
60
61 if s.expires_at() < now || !s.active() {
62 if let Err(err) = self.database.delete_session(token).await {
67 tracing::warn!(
68 error = %err,
69 "Failed to delete expired session; will be retried later"
70 );
71 }
72 return Ok(None);
73 }
74
75 if !self.config.session.disable_session_refresh {
77 match self.config.session.update_age {
78 Some(age) => {
79 let updated = s.updated_at();
82 Utc::now().signed_duration_since(updated) >= age
83 }
84 None => true,
86 }
87 } else {
88 false
89 }
90 } else {
91 false
92 };
93
94 if should_refresh {
95 let new_expires_at = Utc::now() + self.config.session.expires_in;
96 match self
97 .database
98 .update_session_expiry(token, new_expires_at)
99 .await
100 {
101 Ok(()) => {
102 match self.database.get_session(token).await {
108 Ok(Some(refreshed)) => session = Some(refreshed),
109 Ok(None) => {
110 tracing::warn!(
111 "Session re-read after refresh returned None (concurrent revoke?); returning pre-refresh value"
112 );
113 }
114 Err(err) => {
115 tracing::warn!(
116 error = %err,
117 "Session re-read after refresh failed; returning pre-refresh value"
118 );
119 }
120 }
121 }
122 Err(err) => {
123 tracing::warn!(
128 error = %err,
129 "Failed to refresh session expiry; returning pre-refresh session"
130 );
131 }
132 }
133 }
134
135 Ok(session)
136 }
137
138 pub async fn delete_session(&self, token: &str) -> AuthResult<()> {
140 self.database.delete_session(token).await?;
141 Ok(())
142 }
143
144 pub async fn delete_user_sessions(&self, user_id: impl AsRef<str>) -> AuthResult<()> {
146 self.database.delete_user_sessions(user_id.as_ref()).await?;
147 Ok(())
148 }
149
150 pub async fn list_user_sessions(
152 &self,
153 user_id: impl AsRef<str>,
154 ) -> AuthResult<Vec<S::Session>> {
155 let sessions = self.database.get_user_sessions(user_id.as_ref()).await?;
156 let now = Utc::now();
157
158 let active_sessions = sessions
160 .into_iter()
161 .filter(|session| session.expires_at() > now && session.active())
162 .collect();
163
164 Ok(active_sessions)
165 }
166
167 pub async fn revoke_session(&self, token: &str) -> AuthResult<bool> {
169 let session_exists = self.get_session(token).await?.is_some();
171
172 if session_exists {
173 self.delete_session(token).await?;
174 Ok(true)
175 } else {
176 Ok(false)
177 }
178 }
179
180 pub async fn revoke_all_user_sessions(&self, user_id: impl AsRef<str>) -> AuthResult<usize> {
182 let user_id = user_id.as_ref();
184 let sessions = self.list_user_sessions(user_id).await?;
185 let count = sessions.len();
186
187 self.delete_user_sessions(user_id).await?;
188 Ok(count)
189 }
190
191 pub async fn revoke_other_user_sessions(
193 &self,
194 user_id: impl AsRef<str>,
195 current_token: &str,
196 ) -> AuthResult<usize> {
197 let sessions = self.list_user_sessions(user_id).await?;
198 let mut count = 0;
199
200 for session in sessions {
201 if session.token() != current_token {
202 self.delete_session(session.token()).await?;
203 count += 1;
204 }
205 }
206
207 Ok(count)
208 }
209
210 pub async fn cleanup_expired_sessions(&self) -> AuthResult<usize> {
212 let count = self.database.delete_expired_sessions().await?;
213 Ok(count)
214 }
215
216 pub fn is_session_fresh(&self, session: &impl AuthSession) -> bool {
223 match self.config.session.fresh_age {
224 Some(fresh_age) => session.created_at() + fresh_age > Utc::now(),
225 None => false,
226 }
227 }
228
229 pub fn validate_token_format(&self, token: &str) -> bool {
231 token.starts_with("session_") && token.len() > 40
232 }
233
234 pub fn extract_session_token(&self, req: &crate::types::AuthRequest) -> Option<String> {
239 if let Some(auth_header) = req.headers.get("authorization")
241 && let Some(token) = auth_header.strip_prefix("Bearer ")
242 {
243 return Some(token.to_string());
244 }
245
246 if let Some(cookie_header) = req.headers.get("cookie") {
248 let cookie_name = &self.config.session.cookie_name;
249 for c in cookie::Cookie::split_parse(cookie_header).flatten() {
250 if c.name() == cookie_name && !c.value().is_empty() {
251 return Some(c.value().to_string());
252 }
253 }
254 }
255
256 None
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use crate::entity::AuthSession;
264 use crate::test_store::{BundledSchema, test_config, test_database};
265 use crate::types::AuthRequest;
266 use crate::types::HttpMethod;
267 use crate::wire::SessionView;
268 use chrono::Duration;
269
270 fn test_manager() -> SessionManager<BundledSchema> {
271 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
272 SessionManager::new(test_config(), runtime.block_on(test_database()))
273 }
274
275 #[test]
279 fn valid_token_format() {
280 let mgr = test_manager();
281 let token = "session_abcdefghijklmnopqrstuvwxyz1234567890";
282 assert!(mgr.validate_token_format(token));
283 }
284
285 #[test]
287 fn invalid_token_no_prefix() {
288 let mgr = test_manager();
289 assert!(!mgr.validate_token_format("abcdefghijklmnopqrstuvwxyz1234567890"));
290 }
291
292 #[test]
294 fn invalid_token_too_short() {
295 let mgr = test_manager();
296 assert!(!mgr.validate_token_format("session_short"));
297 }
298
299 #[test]
303 fn extract_from_bearer() {
304 let mgr = test_manager();
305 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
306 let _ = req
307 .headers
308 .insert("authorization".into(), "Bearer my-token".into());
309 assert_eq!(mgr.extract_session_token(&req), Some("my-token".into()));
310 }
311
312 #[test]
314 fn extract_from_cookie() {
315 let mgr = test_manager();
316 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
317 let _ = req.headers.insert(
318 "cookie".into(),
319 "better-auth.session_token=tok123; other=val".into(),
320 );
321 assert_eq!(mgr.extract_session_token(&req), Some("tok123".into()));
322 }
323
324 #[test]
326 fn extract_bearer_takes_precedence_over_cookie() {
327 let mgr = test_manager();
328 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
329 let _ = req
330 .headers
331 .insert("authorization".into(), "Bearer bearer-tok".into());
332 let _ = req.headers.insert(
333 "cookie".into(),
334 "better-auth.session_token=cookie-tok".into(),
335 );
336 assert_eq!(mgr.extract_session_token(&req), Some("bearer-tok".into()));
337 }
338
339 #[test]
341 fn extract_returns_none_without_auth() {
342 let mgr = test_manager();
343 let req = AuthRequest::new(HttpMethod::Get, "/test");
344 assert_eq!(mgr.extract_session_token(&req), None);
345 }
346
347 #[test]
349 fn extract_skips_empty_cookie_value() {
350 let mgr = test_manager();
351 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
352 let _ = req
353 .headers
354 .insert("cookie".into(), "better-auth.session_token=".into());
355 assert_eq!(mgr.extract_session_token(&req), None);
356 }
357
358 #[test]
362 fn session_fresh_when_within_window() {
363 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
364 config.session.fresh_age = Some(Duration::minutes(10));
365 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
366 let mgr = SessionManager::new(Arc::new(config), runtime.block_on(test_database()));
367
368 let session = SessionView {
370 id: "s1".into(),
371 expires_at: Utc::now() + Duration::hours(1),
372 token: "tok".into(),
373 created_at: Utc::now(),
374 updated_at: Utc::now(),
375 ip_address: None,
376 user_agent: None,
377 user_id: "u1".into(),
378 impersonated_by: None,
379 active_organization_id: None,
380 active: true,
381 };
382 assert!(mgr.is_session_fresh(&session));
383 }
384
385 #[test]
387 fn session_not_fresh_when_old() {
388 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
389 config.session.fresh_age = Some(Duration::minutes(10));
390 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
391 let mgr = SessionManager::new(Arc::new(config), runtime.block_on(test_database()));
392
393 let session = SessionView {
394 id: "s1".into(),
395 expires_at: Utc::now() + Duration::hours(1),
396 token: "tok".into(),
397 created_at: Utc::now() - Duration::minutes(20),
398 updated_at: Utc::now(),
399 ip_address: None,
400 user_agent: None,
401 user_id: "u1".into(),
402 impersonated_by: None,
403 active_organization_id: None,
404 active: true,
405 };
406 assert!(!mgr.is_session_fresh(&session));
407 }
408
409 #[test]
411 fn session_never_fresh_when_no_fresh_age() {
412 let mgr = test_manager(); let session = SessionView {
414 id: "s1".into(),
415 expires_at: Utc::now() + Duration::hours(1),
416 token: "tok".into(),
417 created_at: Utc::now(),
418 updated_at: Utc::now(),
419 ip_address: None,
420 user_agent: None,
421 user_id: "u1".into(),
422 impersonated_by: None,
423 active_organization_id: None,
424 active: true,
425 };
426 assert!(!mgr.is_session_fresh(&session));
427 }
428
429 #[tokio::test]
433 async fn create_and_get_session() {
434 let db = test_database().await;
435 let mgr = SessionManager::new(test_config(), db.clone());
436
437 let user = db
439 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
440 .await
441 .unwrap();
442
443 let session = mgr.create_session(&user, None, None).await.unwrap();
444 let token = session.token().to_string();
445
446 let retrieved = mgr.get_session(&token).await.unwrap();
447 assert!(retrieved.is_some());
448 }
449
450 #[tokio::test]
452 async fn refresh_returns_the_persisted_expiry() {
453 let db = test_database().await;
454 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
455 config.session.update_age = None;
457 let mgr = SessionManager::new(Arc::new(config), db.clone());
458
459 let user = db
460 .create_user(crate::types::CreateUser::new().with_email("refresh@test.com"))
461 .await
462 .unwrap();
463 let session = mgr.create_session(&user, None, None).await.unwrap();
464 let token = session.token().to_string();
465
466 let stale = session.expires_at() - Duration::minutes(30);
468 db.update_session_expiry(&token, stale).await.unwrap();
469
470 let returned = mgr
471 .get_session(&token)
472 .await
473 .unwrap()
474 .expect("session should still be live");
475 let stored = db
476 .get_session(&token)
477 .await
478 .unwrap()
479 .expect("session should still be stored");
480
481 assert!(
482 returned.expires_at() > stale,
483 "refresh should have extended the expiry"
484 );
485 assert_eq!(
486 returned.expires_at(),
487 stored.expires_at(),
488 "returned session must reflect the persisted expiry, not the pre-refresh value"
489 );
490 }
491
492 #[tokio::test]
494 async fn create_session_without_metadata_uses_empty_strings() {
495 let db = test_database().await;
496 let mgr = SessionManager::new(test_config(), db.clone());
497
498 let user = db
499 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
500 .await
501 .unwrap();
502
503 let session = mgr.create_session(&user, None, None).await.unwrap();
504 assert_eq!(session.ip_address.as_deref(), Some(""));
505 assert_eq!(session.user_agent.as_deref(), Some(""));
506 }
507
508 #[tokio::test]
510 async fn delete_session_removes_it() {
511 let db = test_database().await;
512 let mgr = SessionManager::new(test_config(), db.clone());
513
514 let user = db
515 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
516 .await
517 .unwrap();
518
519 let session = mgr.create_session(&user, None, None).await.unwrap();
520 let token = session.token().to_string();
521
522 mgr.delete_session(&token).await.unwrap();
523 let retrieved = mgr.get_session(&token).await.unwrap();
524 assert!(retrieved.is_none());
525 }
526
527 #[tokio::test]
529 async fn revoke_session_returns_true_when_found() {
530 let db = test_database().await;
531 let mgr = SessionManager::new(test_config(), db.clone());
532
533 let user = db
534 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
535 .await
536 .unwrap();
537
538 let session = mgr.create_session(&user, None, None).await.unwrap();
539 let result = mgr.revoke_session(session.token()).await.unwrap();
540 assert!(result);
541 }
542
543 #[tokio::test]
545 async fn revoke_session_returns_false_when_not_found() {
546 let mgr = SessionManager::new(test_config(), test_database().await);
547 let result = mgr.revoke_session("nonexistent-token").await.unwrap();
548 assert!(!result);
549 }
550
551 #[tokio::test]
553 async fn list_user_sessions_excludes_expired() {
554 let db = test_database().await;
555 let mgr = SessionManager::new(test_config(), db.clone());
556
557 let user = db
558 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
559 .await
560 .unwrap();
561
562 let _ = mgr.create_session(&user, None, None).await.unwrap();
564 let _ = mgr.create_session(&user, None, None).await.unwrap();
565
566 let sessions = mgr.list_user_sessions(user.id()).await.unwrap();
567 assert_eq!(sessions.len(), 2);
568 }
569
570 #[tokio::test]
572 async fn revoke_all_user_sessions() {
573 let db = test_database().await;
574 let mgr = SessionManager::new(test_config(), db.clone());
575
576 let user = db
577 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
578 .await
579 .unwrap();
580
581 let _ = mgr.create_session(&user, None, None).await.unwrap();
582 let _ = mgr.create_session(&user, None, None).await.unwrap();
583
584 let count = mgr.revoke_all_user_sessions(user.id()).await.unwrap();
585 assert_eq!(count, 2);
586
587 let sessions = mgr.list_user_sessions(user.id()).await.unwrap();
588 assert!(sessions.is_empty());
589 }
590
591 #[tokio::test]
593 async fn revoke_other_sessions_keeps_current() {
594 let db = test_database().await;
595 let mgr = SessionManager::new(test_config(), db.clone());
596
597 let user = db
598 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
599 .await
600 .unwrap();
601
602 let current = mgr.create_session(&user, None, None).await.unwrap();
603 let _ = mgr.create_session(&user, None, None).await.unwrap();
604 let _ = mgr.create_session(&user, None, None).await.unwrap();
605
606 let count = mgr
607 .revoke_other_user_sessions(user.id(), current.token())
608 .await
609 .unwrap();
610 assert_eq!(count, 2);
611
612 let remaining = mgr.list_user_sessions(user.id()).await.unwrap();
613 assert_eq!(remaining.len(), 1);
614 assert_eq!(remaining[0].token(), current.token());
615 }
616}