1use std::sync::Arc;
2
3use chrono::Duration;
4use sqlx::SqlitePool;
5
6use crate::db::Db;
7use crate::error::AuthError;
8use crate::sessions::{self, SessionConfig};
9use crate::types::{SessionToken, User};
10
11pub struct LoginOutcome {
13 pub user: User,
14 pub token: SessionToken,
15 pub set_cookie: String,
17}
18
19#[derive(Debug, thiserror::Error)]
21pub enum BuildError {
22 #[error("database error: {0}")]
24 Database(#[from] AuthError),
25
26 #[error("invalid configuration: {0}")]
29 InvalidConfig(&'static str),
30}
31
32enum PoolSource {
33 Url(String),
34 Pool(SqlitePool),
35}
36
37pub struct AllowThemBuilder {
39 pool_source: PoolSource,
40 session_ttl: Option<Duration>,
41 cookie_name: Option<&'static str>,
42 cookie_secure: Option<bool>,
43 cookie_domain: String,
44 mfa_key: Option<[u8; 32]>,
45 signing_key: Option<[u8; 32]>,
46 csrf_key: Option<[u8; 32]>,
47 base_url: Option<String>,
48}
49
50impl AllowThemBuilder {
51 pub fn new(url: impl Into<String>) -> Self {
56 Self {
57 pool_source: PoolSource::Url(url.into()),
58 session_ttl: None,
59 cookie_name: None,
60 cookie_secure: None,
61 cookie_domain: String::new(),
62 mfa_key: None,
63 signing_key: None,
64 csrf_key: None,
65 base_url: None,
66 }
67 }
68
69 pub fn with_pool(pool: SqlitePool) -> Self {
74 Self {
75 pool_source: PoolSource::Pool(pool),
76 session_ttl: None,
77 cookie_name: None,
78 cookie_secure: None,
79 cookie_domain: String::new(),
80 mfa_key: None,
81 signing_key: None,
82 csrf_key: None,
83 base_url: None,
84 }
85 }
86
87 pub fn session_ttl(mut self, ttl: Duration) -> Self {
89 self.session_ttl = Some(ttl);
90 self
91 }
92
93 pub fn cookie_name(mut self, name: &'static str) -> Self {
95 self.cookie_name = Some(name);
96 self
97 }
98
99 pub fn cookie_secure(mut self, secure: bool) -> Self {
103 self.cookie_secure = Some(secure);
104 self
105 }
106
107 pub fn cookie_domain(mut self, domain: impl Into<String>) -> Self {
112 self.cookie_domain = domain.into();
113 self
114 }
115
116 pub fn mfa_key(mut self, key: [u8; 32]) -> Self {
121 self.mfa_key = Some(key);
122 self
123 }
124
125 pub fn signing_key(mut self, key: [u8; 32]) -> Self {
130 self.signing_key = Some(key);
131 self
132 }
133
134 pub fn base_url(mut self, url: impl Into<String>) -> Self {
140 self.base_url = Some(url.into());
141 self
142 }
143
144 pub fn csrf_key(mut self, key: [u8; 32]) -> Self {
150 self.csrf_key = Some(key);
151 self
152 }
153
154 pub async fn build(self) -> Result<AllowThem, BuildError> {
159 let db = match self.pool_source {
160 PoolSource::Url(url) => Db::connect(&url).await?,
161 PoolSource::Pool(pool) => Db::new(pool).await?,
162 };
163
164 let defaults = SessionConfig::default();
165 let session_config = SessionConfig {
166 ttl: self.session_ttl.unwrap_or(defaults.ttl),
167 cookie_name: self.cookie_name.unwrap_or(defaults.cookie_name),
168 secure: self.cookie_secure.unwrap_or(defaults.secure),
169 };
170
171 Ok(AllowThem {
172 inner: Arc::new(Inner {
173 db,
174 session_config,
175 cookie_domain: self.cookie_domain,
176 mfa_key: self.mfa_key,
177 signing_key: self.signing_key,
178 csrf_key: self.csrf_key,
179 base_url: self.base_url,
180 }),
181 })
182 }
183}
184
185struct Inner {
186 db: Db,
187 session_config: SessionConfig,
188 cookie_domain: String,
189 mfa_key: Option<[u8; 32]>,
190 signing_key: Option<[u8; 32]>,
191 csrf_key: Option<[u8; 32]>,
192 base_url: Option<String>,
193}
194
195#[derive(Clone)]
201pub struct AllowThem {
202 inner: Arc<Inner>,
203}
204
205impl AllowThem {
206 pub fn db(&self) -> &Db {
211 &self.inner.db
212 }
213
214 pub fn session_config(&self) -> &SessionConfig {
216 &self.inner.session_config
217 }
218
219 pub fn session_cookie(&self, token: &SessionToken) -> String {
224 sessions::session_cookie(token, &self.inner.session_config, &self.inner.cookie_domain)
225 }
226
227 pub(crate) fn mfa_key(&self) -> Result<&[u8; 32], AuthError> {
229 self.inner
230 .mfa_key
231 .as_ref()
232 .ok_or(AuthError::MfaNotConfigured)
233 }
234
235 pub(crate) fn signing_key(&self) -> Result<&[u8; 32], AuthError> {
237 self.inner
238 .signing_key
239 .as_ref()
240 .ok_or(AuthError::SigningKeyNotConfigured)
241 }
242
243 pub fn base_url(&self) -> Result<&str, AuthError> {
245 self.inner
246 .base_url
247 .as_deref()
248 .ok_or(AuthError::BaseUrlNotConfigured)
249 }
250
251 pub fn csrf_key(&self) -> Result<&[u8; 32], AuthError> {
252 self.inner
253 .csrf_key
254 .as_ref()
255 .ok_or(AuthError::CsrfKeyNotConfigured)
256 }
257
258 pub async fn get_decrypted_signing_key(
263 &self,
264 ) -> Result<(crate::signing_keys::SigningKey, String), AuthError> {
265 let enc_key = self.signing_key()?;
266 let key = self.db().get_active_signing_key().await?;
267 let pem = crate::signing_keys::decrypt_private_key(&key, enc_key)?;
268 Ok((key, pem))
269 }
270
271 pub fn clear_session_cookie(&self) -> String {
277 sessions::clear_session_cookie(&self.inner.session_config, &self.inner.cookie_domain)
278 }
279
280 pub fn parse_session_cookie(&self, cookie_header: &str) -> Option<SessionToken> {
284 sessions::parse_session_cookie(cookie_header, self.inner.session_config.cookie_name)
285 }
286
287 pub async fn login(&self, identifier: &str, password: &str) -> Result<LoginOutcome, AuthError> {
297 use chrono::Utc;
298
299 use crate::audit::AuditEvent;
300 use crate::password::verify_password;
301
302 let user = self
303 .db()
304 .find_for_login(identifier)
305 .await
306 .map_err(|e| match e {
307 AuthError::NotFound => AuthError::InvalidCredentials,
308 other => other,
309 })?;
310
311 if !user.is_active {
312 return Err(AuthError::InvalidCredentials);
313 }
314
315 let hash = user
316 .password_hash
317 .as_ref()
318 .ok_or(AuthError::InvalidCredentials)?;
319
320 if !verify_password(password, hash)? {
321 return Err(AuthError::InvalidCredentials);
322 }
323
324 let token = sessions::generate_token();
325 let token_hash = sessions::hash_token(&token);
326 let expires_at = Utc::now() + self.inner.session_config.ttl;
327 self.db()
328 .create_session(user.id, token_hash, None, None, expires_at)
329 .await?;
330
331 let _ = self
332 .db()
333 .log_audit(AuditEvent::Login, Some(&user.id), None, None, None, None)
334 .await;
335
336 let set_cookie = self.session_cookie(&token);
337 Ok(LoginOutcome {
338 user,
339 token,
340 set_cookie,
341 })
342 }
343
344 pub async fn create_session_cookie(
350 &self,
351 user_id: crate::types::UserId,
352 ) -> Result<LoginOutcome, AuthError> {
353 use chrono::Utc;
354
355 let user = self.db().get_user(user_id).await?;
356 let token = sessions::generate_token();
357 let token_hash = sessions::hash_token(&token);
358 let expires_at = Utc::now() + self.inner.session_config.ttl;
359 self.db()
360 .create_session(user_id, token_hash, None, None, expires_at)
361 .await?;
362
363 let set_cookie = self.session_cookie(&token);
364 Ok(LoginOutcome {
365 user,
366 token,
367 set_cookie,
368 })
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use sqlx::sqlite::SqliteConnectOptions;
375 use std::str::FromStr;
376
377 use super::*;
378 use crate::sessions::generate_token;
379 use crate::types::Email;
380
381 #[tokio::test]
382 async fn build_with_url_defaults() {
383 let ath = AllowThemBuilder::new("sqlite::memory:")
384 .build()
385 .await
386 .unwrap();
387
388 let config = ath.session_config();
389 assert_eq!(config.ttl, Duration::hours(24));
390 assert_eq!(config.cookie_name, "allowthem_session");
391 assert!(config.secure);
392
393 let token = generate_token();
394 let cookie = ath.session_cookie(&token);
395 assert!(!cookie.contains("; Domain="));
396 }
397
398 #[tokio::test]
399 async fn build_with_pool() {
400 let opts = SqliteConnectOptions::from_str("sqlite::memory:")
401 .unwrap()
402 .pragma("foreign_keys", "ON");
403 let pool = sqlx::SqlitePool::connect_with(opts).await.unwrap();
404
405 let ath = AllowThemBuilder::with_pool(pool).build().await.unwrap();
406
407 let email = Email::new("test@example.com".into()).unwrap();
408 let user = ath.db().create_user(email, "password123", None, None).await;
409 assert!(user.is_ok());
410 }
411
412 #[tokio::test]
413 async fn build_with_overrides() {
414 let ath = AllowThemBuilder::new("sqlite::memory:")
415 .session_ttl(Duration::hours(48))
416 .cookie_name("my_session")
417 .cookie_secure(false)
418 .cookie_domain("example.com")
419 .build()
420 .await
421 .unwrap();
422
423 let config = ath.session_config();
424 assert_eq!(config.ttl, Duration::hours(48));
425 assert_eq!(config.cookie_name, "my_session");
426 assert!(!config.secure);
427 }
428
429 #[tokio::test]
430 async fn session_cookie_uses_config() {
431 let ath = AllowThemBuilder::new("sqlite::memory:")
432 .cookie_name("custom")
433 .cookie_secure(false)
434 .cookie_domain("example.com")
435 .build()
436 .await
437 .unwrap();
438
439 let token = generate_token();
440 let cookie = ath.session_cookie(&token);
441
442 assert!(cookie.contains("custom="));
443 assert!(cookie.contains("; Domain=example.com"));
444 assert!(!cookie.contains("; Secure"));
445 }
446
447 #[tokio::test]
448 async fn clear_session_cookie_defaults() {
449 let ath = AllowThemBuilder::new("sqlite::memory:")
450 .build()
451 .await
452 .unwrap();
453
454 let cookie = ath.clear_session_cookie();
455 assert!(cookie.starts_with("allowthem_session=;"));
456 assert!(cookie.contains("; Max-Age=0"));
457 assert!(!cookie.contains("; Domain="));
458 assert!(cookie.contains("; Secure"));
459 }
460
461 #[tokio::test]
462 async fn clear_session_cookie_name_matches_session_cookie() {
463 let ath = AllowThemBuilder::new("sqlite::memory:")
464 .cookie_name("app_session")
465 .build()
466 .await
467 .unwrap();
468
469 let token = generate_token();
470 let set = ath.session_cookie(&token);
471 let clear = ath.clear_session_cookie();
472
473 assert!(set.starts_with("app_session="));
475 assert!(clear.starts_with("app_session=;"));
476 assert!(clear.contains("; Path=/"));
477 assert!(clear.contains("; Max-Age=0"));
478 }
479
480 #[tokio::test]
481 async fn clear_session_cookie_with_domain_and_no_secure() {
482 let ath = AllowThemBuilder::new("sqlite::memory:")
483 .cookie_name("my_session")
484 .cookie_secure(false)
485 .cookie_domain("example.com")
486 .build()
487 .await
488 .unwrap();
489
490 let cookie = ath.clear_session_cookie();
491 assert!(cookie.starts_with("my_session=;"));
492 assert!(cookie.contains("; Max-Age=0"));
493 assert!(cookie.contains("; Domain=example.com"));
494 assert!(!cookie.contains("; Secure"));
495 }
496
497 #[tokio::test]
498 async fn parse_session_cookie_uses_config() {
499 let ath = AllowThemBuilder::new("sqlite::memory:")
500 .cookie_name("custom")
501 .build()
502 .await
503 .unwrap();
504
505 let header = "custom=abc123; other=xyz";
506 let result = ath.parse_session_cookie(header);
507
508 assert!(result.is_some());
509 assert_eq!(result.unwrap().as_str(), "abc123");
510 }
511
512 #[tokio::test]
513 async fn build_with_bad_url_fails() {
514 let result = AllowThemBuilder::new("not-a-url").build().await;
515
516 assert!(result.is_err());
517 assert!(matches!(result.err().unwrap(), BuildError::Database(_)));
518 }
519
520 #[tokio::test]
521 async fn clone_shares_state() {
522 let ath = AllowThemBuilder::new("sqlite::memory:")
523 .build()
524 .await
525 .unwrap();
526 let ath2 = ath.clone();
527
528 let email = Email::new("shared@example.com".into()).unwrap();
529 let user = ath
530 .db()
531 .create_user(email, "password123", None, None)
532 .await
533 .unwrap();
534
535 let found = ath2.db().get_user(user.id).await;
536 assert!(found.is_ok());
537 assert_eq!(found.unwrap().id, user.id);
538 }
539
540 #[tokio::test]
541 async fn signing_key_not_configured_returns_error() {
542 let ath = AllowThemBuilder::new("sqlite::memory:")
543 .build()
544 .await
545 .unwrap();
546 let result = ath.signing_key();
547 assert!(matches!(
548 result,
549 Err(crate::error::AuthError::SigningKeyNotConfigured)
550 ));
551 }
552
553 #[tokio::test]
554 async fn base_url_not_configured_returns_error() {
555 let ath = AllowThemBuilder::new("sqlite::memory:")
556 .build()
557 .await
558 .unwrap();
559 let result = ath.base_url();
560 assert!(matches!(
561 result,
562 Err(crate::error::AuthError::BaseUrlNotConfigured)
563 ));
564 }
565
566 #[tokio::test]
567 async fn base_url_configured_returns_value() {
568 let ath = AllowThemBuilder::new("sqlite::memory:")
569 .base_url("https://auth.example.com")
570 .build()
571 .await
572 .unwrap();
573 let result = ath.base_url();
574 assert!(matches!(result, Ok("https://auth.example.com")));
575 }
576
577 #[tokio::test]
578 async fn login_success() {
579 let ath = AllowThemBuilder::new("sqlite::memory:")
580 .cookie_secure(false)
581 .build()
582 .await
583 .unwrap();
584
585 let email = Email::new("login@example.com".into()).unwrap();
586 ath.db()
587 .create_user(email, "secret", None, None)
588 .await
589 .unwrap();
590
591 let outcome = ath.login("login@example.com", "secret").await.unwrap();
592 assert_eq!(outcome.user.email.as_str(), "login@example.com");
593 assert!(!outcome.token.as_str().is_empty());
594 assert!(outcome.set_cookie.contains("allowthem_session="));
595 }
596
597 #[tokio::test]
598 async fn login_wrong_password() {
599 let ath = AllowThemBuilder::new("sqlite::memory:")
600 .build()
601 .await
602 .unwrap();
603
604 let email = Email::new("wp@example.com".into()).unwrap();
605 ath.db()
606 .create_user(email, "correct", None, None)
607 .await
608 .unwrap();
609
610 let result = ath.login("wp@example.com", "wrong").await;
611 assert!(matches!(result, Err(AuthError::InvalidCredentials)));
612 }
613
614 #[tokio::test]
615 async fn login_unknown_identifier() {
616 let ath = AllowThemBuilder::new("sqlite::memory:")
617 .build()
618 .await
619 .unwrap();
620
621 let result = ath.login("nobody@example.com", "any").await;
622 assert!(matches!(result, Err(AuthError::InvalidCredentials)));
623 }
624
625 #[tokio::test]
626 async fn login_inactive_user() {
627 let ath = AllowThemBuilder::new("sqlite::memory:")
628 .build()
629 .await
630 .unwrap();
631
632 let email = Email::new("inactive@example.com".into()).unwrap();
633 let user = ath
634 .db()
635 .create_user(email, "secret", None, None)
636 .await
637 .unwrap();
638 ath.db().update_user_active(user.id, false).await.unwrap();
639
640 let result = ath.login("inactive@example.com", "secret").await;
641 assert!(matches!(result, Err(AuthError::InvalidCredentials)));
642 }
643
644 #[tokio::test]
645 async fn login_no_password_hash() {
646 use crate::types::UserId;
647
648 let ath = AllowThemBuilder::new("sqlite::memory:")
649 .build()
650 .await
651 .unwrap();
652
653 let id = UserId::new();
656 let now = chrono::Utc::now()
657 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
658 .to_string();
659 sqlx::query(
660 "INSERT INTO allowthem_users \
661 (id, email, username, password_hash, email_verified, is_active, created_at, updated_at) \
662 VALUES (?, 'sso@example.com', NULL, NULL, 1, 1, ?, ?)",
663 )
664 .bind(id)
665 .bind(&now)
666 .bind(&now)
667 .execute(ath.db().pool())
668 .await
669 .unwrap();
670
671 let result = ath.login("sso@example.com", "any").await;
672 assert!(matches!(result, Err(AuthError::InvalidCredentials)));
673 }
674
675 #[tokio::test]
676 async fn create_session_cookie_success() {
677 let ath = AllowThemBuilder::new("sqlite::memory:")
678 .cookie_secure(false)
679 .build()
680 .await
681 .unwrap();
682
683 let email = Email::new("sess@example.com".into()).unwrap();
684 let user = ath
685 .db()
686 .create_user(email, "secret", None, None)
687 .await
688 .unwrap();
689
690 let outcome = ath.create_session_cookie(user.id).await.unwrap();
691 assert_eq!(outcome.user.id, user.id);
692 assert!(!outcome.token.as_str().is_empty());
693 assert!(outcome.set_cookie.contains("allowthem_session="));
694
695 let session = ath.db().lookup_session(&outcome.token).await.unwrap();
697 assert!(session.is_some());
698 }
699
700 #[tokio::test]
701 async fn create_session_cookie_unknown_user() {
702 use crate::types::UserId;
703
704 let ath = AllowThemBuilder::new("sqlite::memory:")
705 .build()
706 .await
707 .unwrap();
708
709 let result = ath.create_session_cookie(UserId::new()).await;
710 assert!(matches!(result, Err(AuthError::NotFound)));
711 }
712}