cot/auth/
db.rs

1//! Database-backed user authentication backend.
2//!
3//! This module provides a user type and an authentication backend that stores
4//! the user data in a database using the Cot ORM.
5
6use std::any::Any;
7use std::borrow::Cow;
8use std::fmt::{Display, Formatter};
9use std::sync::Arc;
10
11use async_trait::async_trait;
12// Importing `Auto` from `cot` instead of `crate` so that the migration generator
13// can figure out it's an autogenerated field
14use cot::db::Auto;
15use cot_macros::AdminModel;
16use hmac::{Hmac, Mac};
17use sha2::Sha512;
18use thiserror::Error;
19
20use crate::App;
21use crate::admin::{AdminModelManager, DefaultAdminModelManager};
22use crate::auth::{
23    AuthBackend, AuthError, Password, PasswordHash, PasswordVerificationResult, Result,
24    SessionAuthHash, User, UserId,
25};
26use crate::config::SecretKey;
27use crate::db::migrations::SyncDynMigration;
28use crate::db::{Database, DatabaseBackend, LimitedString, Model, model, query};
29use crate::form::Form;
30
31pub mod migrations;
32
33pub(crate) const MAX_USERNAME_LENGTH: u32 = 255;
34
35/// A user stored in the database.
36#[derive(Debug, Clone, Form, AdminModel)]
37#[model]
38pub struct DatabaseUser {
39    #[model(primary_key)]
40    id: Auto<i64>,
41    #[model(unique)]
42    username: LimitedString<MAX_USERNAME_LENGTH>,
43    password: PasswordHash,
44}
45
46/// An error that occurs when creating a user.
47#[derive(Debug, Clone, Error)]
48#[non_exhaustive]
49pub enum CreateUserError {
50    /// The username is too long.
51    #[error("username is too long (max {MAX_USERNAME_LENGTH} characters, got {0})")]
52    UsernameTooLong(usize),
53}
54
55impl DatabaseUser {
56    #[must_use]
57    fn new(
58        id: Auto<i64>,
59        username: LimitedString<MAX_USERNAME_LENGTH>,
60        password: &Password,
61    ) -> Self {
62        Self {
63            id,
64            username,
65            password: PasswordHash::from_password(password),
66        }
67    }
68
69    /// Create a new user and save it to the database.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the user could not be saved to the database.
74    ///
75    /// # Example
76    ///
77    /// ```
78    /// use cot::auth::Password;
79    /// use cot::auth::db::DatabaseUser;
80    /// use cot::request::{Request, RequestExt};
81    /// use cot::response::{Response, ResponseExt};
82    /// use cot::{Body, StatusCode};
83    ///
84    /// async fn view(request: &Request) -> cot::Result<Response> {
85    ///     let user = DatabaseUser::create_user(
86    ///         request.db(),
87    ///         "testuser".to_string(),
88    ///         &Password::new("password123"),
89    ///     )
90    ///     .await?;
91    ///
92    ///     Ok(Response::new_html(
93    ///         StatusCode::OK,
94    ///         Body::fixed("User created!"),
95    ///     ))
96    /// }
97    ///
98    /// # #[tokio::main]
99    /// # async fn main() -> cot::Result<()> {
100    /// #     use cot::test::{TestDatabase, TestRequestBuilder};
101    /// #     let mut test_database = TestDatabase::new_sqlite().await?;
102    /// #     test_database.with_auth().run_migrations().await;
103    /// #     let request = TestRequestBuilder::get("/")
104    /// #         .with_db_auth(test_database.database())
105    /// #         .await
106    /// #         .build();
107    /// #     view(&request).await?;
108    /// #     test_database.cleanup().await?;
109    /// #     Ok(())
110    /// # }
111    /// ```
112    pub async fn create_user<DB: DatabaseBackend, T: Into<String>, U: Into<Password>>(
113        db: &DB,
114        username: T,
115        password: U,
116    ) -> Result<Self> {
117        let username = username.into();
118        let username_length = username.len();
119        let username = LimitedString::<MAX_USERNAME_LENGTH>::new(username).map_err(|_| {
120            AuthError::backend_error(CreateUserError::UsernameTooLong(username_length))
121        })?;
122
123        let mut user = Self::new(Auto::auto(), username, &password.into());
124        user.insert(db).await.map_err(AuthError::backend_error)?;
125
126        Ok(user)
127    }
128
129    /// Get a user by their integer ID. Returns [`None`] if the user does not
130    /// exist.
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if there was an error querying the database.
135    ///
136    /// # Panics
137    ///
138    /// Panics if the user ID provided is not an integer.
139    ///
140    /// # Example
141    ///
142    /// ```
143    /// use cot::auth::db::DatabaseUser;
144    /// use cot::auth::{Password, UserId};
145    /// use cot::request::{Request, RequestExt};
146    /// use cot::response::{Response, ResponseExt};
147    /// use cot::{Body, StatusCode};
148    ///
149    /// async fn view(request: &Request) -> cot::Result<Response> {
150    ///     let user = DatabaseUser::create_user(
151    ///         request.db(),
152    ///         "testuser".to_string(),
153    ///         &Password::new("password123"),
154    ///     )
155    ///     .await?;
156    ///
157    ///     let user_from_db = DatabaseUser::get_by_id(request.db(), user.id()).await?;
158    ///
159    ///     Ok(Response::new_html(
160    ///         StatusCode::OK,
161    ///         Body::fixed("User created!"),
162    ///     ))
163    /// }
164    ///
165    /// # #[tokio::main]
166    /// # async fn main() -> cot::Result<()> {
167    /// #     use cot::test::{TestDatabase, TestRequestBuilder};
168    /// #     let mut test_database = TestDatabase::new_sqlite().await?;
169    /// #     test_database.with_auth().run_migrations().await;
170    /// #     let request = TestRequestBuilder::get("/")
171    /// #         .with_db_auth(test_database.database())
172    /// #         .await
173    /// #         .build();
174    /// #     view(&request).await?;
175    /// #     test_database.cleanup().await?;
176    /// #     Ok(())
177    /// # }
178    /// ```
179    pub async fn get_by_id<DB: DatabaseBackend>(db: &DB, id: i64) -> Result<Option<Self>> {
180        let db_user = query!(DatabaseUser, $id == id)
181            .get(db)
182            .await
183            .map_err(AuthError::backend_error)?;
184
185        Ok(db_user)
186    }
187
188    /// Get a user by their username. Returns [`None`] if the user does not
189    /// exist.
190    ///
191    /// # Errors
192    ///
193    /// Returns an error if there was an error querying the database.
194    ///
195    /// # Example
196    ///
197    /// ```
198    /// use cot::auth::db::DatabaseUser;
199    /// use cot::auth::{Password, UserId};
200    /// use cot::request::{Request, RequestExt};
201    /// use cot::response::{Response, ResponseExt};
202    /// use cot::{Body, StatusCode};
203    ///
204    /// async fn view(request: &Request) -> cot::Result<Response> {
205    ///     let user = DatabaseUser::create_user(
206    ///         request.db(),
207    ///         "testuser".to_string(),
208    ///         &Password::new("password123"),
209    ///     )
210    ///     .await?;
211    ///
212    ///     let user_from_db = DatabaseUser::get_by_username(request.db(), "testuser").await?;
213    ///
214    ///     Ok(Response::new_html(
215    ///         StatusCode::OK,
216    ///         Body::fixed("User created!"),
217    ///     ))
218    /// }
219    ///
220    /// # #[tokio::main]
221    /// # async fn main() -> cot::Result<()> {
222    /// #     use cot::test::{TestDatabase, TestRequestBuilder};
223    /// #     let mut test_database = TestDatabase::new_sqlite().await?;
224    /// #     test_database.with_auth().run_migrations().await;
225    /// #     let request = TestRequestBuilder::get("/")
226    /// #         .with_db_auth(test_database.database())
227    /// #         .await
228    /// #         .build();
229    /// #     view(&request).await?;
230    /// #     test_database.cleanup().await?;
231    /// #     Ok(())
232    /// # }
233    /// ```
234    pub async fn get_by_username<DB: DatabaseBackend>(
235        db: &DB,
236        username: &str,
237    ) -> Result<Option<Self>> {
238        let username = LimitedString::<MAX_USERNAME_LENGTH>::new(username).map_err(|_| {
239            AuthError::backend_error(CreateUserError::UsernameTooLong(username.len()))
240        })?;
241        let db_user = query!(DatabaseUser, $username == username)
242            .get(db)
243            .await
244            .map_err(AuthError::backend_error)?;
245
246        Ok(db_user)
247    }
248
249    /// Authenticate a user.
250    ///
251    /// # Errors
252    ///
253    /// Returns an error if there was an error querying the database.
254    pub async fn authenticate<DB: DatabaseBackend>(
255        db: &DB,
256        credentials: &DatabaseUserCredentials,
257    ) -> Result<Option<Self>> {
258        let username = credentials.username();
259        let username_limited = LimitedString::<MAX_USERNAME_LENGTH>::new(username.to_string())
260            .map_err(|_| {
261                AuthError::backend_error(CreateUserError::UsernameTooLong(username.len()))
262            })?;
263        let user = query!(DatabaseUser, $username == username_limited)
264            .get(db)
265            .await
266            .map_err(AuthError::backend_error)?;
267
268        if let Some(mut user) = user {
269            let password_hash = &user.password;
270            match password_hash.verify(credentials.password()) {
271                PasswordVerificationResult::Ok => Ok(Some(user)),
272                PasswordVerificationResult::OkObsolete(new_hash) => {
273                    user.password = new_hash;
274                    user.save(db).await.map_err(AuthError::backend_error)?;
275                    Ok(Some(user))
276                }
277                PasswordVerificationResult::Invalid => Ok(None),
278            }
279        } else {
280            // SECURITY: If no user was found, run the same hashing function to prevent
281            // timing attacks from being used to determine if a user exists. Additionally,
282            // do something with the result to prevent the compiler from optimizing out the
283            // operation.
284            // TODO: benchmark this to make sure it works as expected
285            let dummy_hash = PasswordHash::from_password(credentials.password());
286            if let PasswordVerificationResult::Invalid = dummy_hash.verify(credentials.password()) {
287                unreachable!(
288                    "Password hash verification should never fail for a newly generated hash"
289                );
290            }
291            Ok(None)
292        }
293    }
294
295    /// Get the ID of the user.
296    ///
297    /// # Example
298    ///
299    /// ```
300    /// use cot::auth::db::DatabaseUser;
301    /// use cot::auth::{Password, UserId};
302    /// use cot::request::{Request, RequestExt};
303    /// use cot::response::{Response, ResponseExt};
304    /// use cot::{Body, StatusCode};
305    ///
306    /// async fn view(request: &Request) -> cot::Result<Response> {
307    ///     let user = DatabaseUser::create_user(
308    ///         request.db(),
309    ///         "testuser".to_string(),
310    ///         &Password::new("password123"),
311    ///     )
312    ///     .await?;
313    ///
314    ///     Ok(Response::new_html(
315    ///         StatusCode::OK,
316    ///         Body::fixed(format!("User ID: {}", user.id())),
317    ///     ))
318    /// }
319    ///
320    /// # #[tokio::main]
321    /// # async fn main() -> cot::Result<()> {
322    /// #     use cot::test::{TestDatabase, TestRequestBuilder};
323    /// #     let mut test_database = TestDatabase::new_sqlite().await?;
324    /// #     test_database.with_auth().run_migrations().await;
325    /// #     let request = TestRequestBuilder::get("/")
326    /// #         .with_db_auth(test_database.database())
327    /// #         .await
328    /// #         .build();
329    /// #     view(&request).await?;
330    /// #     test_database.cleanup().await?;
331    /// #     Ok(())
332    /// # }
333    /// ```
334    #[must_use]
335    pub fn id(&self) -> i64 {
336        match self.id {
337            Auto::Fixed(id) => id,
338            Auto::Auto => unreachable!("DatabaseUser constructed with an unknown ID"),
339        }
340    }
341
342    /// Get the username of the user.
343    ///
344    /// # Example
345    ///
346    /// ```
347    /// use cot::auth::db::DatabaseUser;
348    /// use cot::auth::{Password, UserId};
349    /// use cot::request::{Request, RequestExt};
350    /// use cot::response::{Response, ResponseExt};
351    /// use cot::{Body, StatusCode};
352    ///
353    /// async fn view(request: &Request) -> cot::Result<Response> {
354    ///     let user = DatabaseUser::create_user(
355    ///         request.db(),
356    ///         "testuser".to_string(),
357    ///         &Password::new("password123"),
358    ///     )
359    ///     .await?;
360    ///
361    ///     Ok(Response::new_html(
362    ///         StatusCode::OK,
363    ///         Body::fixed(user.username().to_string()),
364    ///     ))
365    /// }
366    ///
367    /// # #[tokio::main]
368    /// # async fn main() -> cot::Result<()> {
369    /// #     use cot::test::{TestDatabase, TestRequestBuilder};
370    /// #     let mut test_database = TestDatabase::new_sqlite().await?;
371    /// #     test_database.with_auth().run_migrations().await;
372    /// #     let request = TestRequestBuilder::get("/")
373    /// #         .with_db_auth(test_database.database())
374    /// #         .await
375    /// #         .build();
376    /// #     view(&request).await?;
377    /// #     test_database.cleanup().await?;
378    /// #     Ok(())
379    /// # }
380    /// ```
381    #[must_use]
382    pub fn username(&self) -> &str {
383        &self.username
384    }
385}
386
387type SessionAuthHmac = Hmac<Sha512>;
388
389impl User for DatabaseUser {
390    fn id(&self) -> Option<UserId> {
391        Some(UserId::Int(self.id()))
392    }
393
394    fn username(&self) -> Option<Cow<'_, str>> {
395        Some(Cow::from(self.username.as_str()))
396    }
397
398    fn is_active(&self) -> bool {
399        true
400    }
401
402    fn is_authenticated(&self) -> bool {
403        true
404    }
405
406    fn session_auth_hash(&self, secret_key: &SecretKey) -> Option<SessionAuthHash> {
407        let mut mac = SessionAuthHmac::new_from_slice(secret_key.as_bytes())
408            .expect("HMAC can take key of any size");
409        mac.update(self.password.as_str().as_bytes());
410        let hmac_data = mac.finalize().into_bytes();
411
412        Some(SessionAuthHash::new(&hmac_data))
413    }
414}
415
416impl Display for DatabaseUser {
417    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
418        write!(f, "{}", self.username)
419    }
420}
421
422/// Credentials for authenticating a user stored in the database.
423///
424/// This struct is used to authenticate a user stored in the database. It
425/// contains the username and password of the user.
426///
427/// Can be passed to
428/// [`AuthRequestExt::authenticate`](crate::auth::AuthRequestExt::authenticate)
429/// to authenticate a user when using the [`DatabaseUserBackend`].
430#[derive(Debug, Clone)]
431pub struct DatabaseUserCredentials {
432    username: String,
433    password: Password,
434}
435
436impl DatabaseUserCredentials {
437    /// Create a new instance of the database user credentials.
438    ///
439    /// # Example
440    ///
441    /// ```
442    /// use cot::auth::Password;
443    /// use cot::auth::db::DatabaseUserCredentials;
444    ///
445    /// let credentials =
446    ///     DatabaseUserCredentials::new(String::from("testuser"), Password::new("password123"));
447    /// ```
448    #[must_use]
449    pub fn new(username: String, password: Password) -> Self {
450        Self { username, password }
451    }
452
453    /// Get the username of the user.
454    ///
455    /// # Example
456    ///
457    /// ```
458    /// use cot::auth::Password;
459    /// use cot::auth::db::DatabaseUserCredentials;
460    ///
461    /// let credentials =
462    ///     DatabaseUserCredentials::new(String::from("testuser"), Password::new("password123"));
463    /// assert_eq!(credentials.username(), "testuser");
464    /// ```
465    #[must_use]
466    pub fn username(&self) -> &str {
467        &self.username
468    }
469
470    /// Get the password of the user.
471    ///
472    /// # Example
473    ///
474    /// ```
475    /// use cot::auth::Password;
476    /// use cot::auth::db::DatabaseUserCredentials;
477    ///
478    /// let credentials =
479    ///     DatabaseUserCredentials::new(String::from("testuser"), Password::new("password123"));
480    /// assert!(!credentials.password().as_str().is_empty());
481    /// ```
482    #[must_use]
483    pub fn password(&self) -> &Password {
484        &self.password
485    }
486}
487
488/// The authentication backend for users stored in the database.
489///
490/// This is the default authentication backend for Cot. It authenticates
491/// users stored in the database using the [`DatabaseUser`] model.
492///
493/// This backend supports authenticating users using the
494/// [`DatabaseUserCredentials`] struct and ignores all other credential types.
495#[derive(Debug, Clone)]
496pub struct DatabaseUserBackend {
497    database: Arc<Database>,
498}
499
500impl DatabaseUserBackend {
501    /// Create a new instance of the database user authentication backend.
502    ///
503    /// # Example
504    ///
505    /// ```
506    /// use std::sync::Arc;
507    ///
508    /// use cot::auth::AuthBackend;
509    /// use cot::auth::db::DatabaseUserBackend;
510    /// use cot::config::ProjectConfig;
511    /// use cot::project::{AuthBackendContext, WithApps};
512    /// use cot::{Project, ProjectContext};
513    ///
514    /// struct HelloProject;
515    /// impl Project for HelloProject {
516    ///     fn auth_backend(&self, context: &AuthBackendContext) -> Arc<dyn AuthBackend> {
517    ///         Arc::new(DatabaseUserBackend::new(context.database().clone()))
518    ///         // note that it's usually better to just set the auth backend in the config
519    ///     }
520    /// }
521    /// ```
522    #[must_use]
523    pub fn new(database: Arc<Database>) -> Self {
524        Self { database }
525    }
526}
527
528#[async_trait]
529impl AuthBackend for DatabaseUserBackend {
530    async fn authenticate(
531        &self,
532        credentials: &(dyn Any + Send + Sync),
533    ) -> Result<Option<Box<dyn User + Send + Sync>>> {
534        if let Some(credentials) = credentials.downcast_ref::<DatabaseUserCredentials>() {
535            #[expect(trivial_casts)] // Upcast to the correct Box type
536            Ok(DatabaseUser::authenticate(&self.database, credentials)
537                .await
538                .map(|user| user.map(|user| Box::new(user) as Box<dyn User + Send + Sync>))?)
539        } else {
540            Err(AuthError::CredentialsTypeNotSupported)
541        }
542    }
543
544    async fn get_by_id(&self, id: UserId) -> Result<Option<Box<dyn User + Send + Sync>>> {
545        let UserId::Int(id) = id else {
546            return Err(AuthError::UserIdTypeNotSupported);
547        };
548
549        #[expect(trivial_casts)] // Upcast to the correct Box type
550        Ok(DatabaseUser::get_by_id(&self.database, id)
551            .await?
552            .map(|user| Box::new(user) as Box<dyn User + Send + Sync>))
553    }
554}
555
556/// An app that provides authentication via a user model stored in the database.
557#[derive(Debug, Copy, Clone)]
558pub struct DatabaseUserApp;
559
560impl Default for DatabaseUserApp {
561    fn default() -> Self {
562        Self::new()
563    }
564}
565
566impl DatabaseUserApp {
567    /// Create a new instance of the database user authentication app.
568    ///
569    /// # Example
570    ///
571    /// ```no_run
572    /// use cot::auth::db::DatabaseUserApp;
573    /// use cot::config::{DatabaseConfig, ProjectConfig};
574    /// use cot::project::RegisterAppsContext;
575    /// use cot::{App, AppBuilder, Project};
576    ///
577    /// struct HelloProject;
578    /// impl Project for HelloProject {
579    ///     fn config(&self, config_name: &str) -> cot::Result<ProjectConfig> {
580    ///         Ok(ProjectConfig::builder()
581    ///             .database(DatabaseConfig::builder().url("sqlite::memory:").build())
582    ///             .build())
583    ///     }
584    ///
585    ///     fn register_apps(&self, apps: &mut AppBuilder, _context: &RegisterAppsContext) {
586    ///         use cot::project::{RegisterAppsContext, WithConfig};
587    ///         apps.register_with_views(DatabaseUserApp::new(), "");
588    ///     }
589    /// }
590    ///
591    /// #[cot::main]
592    /// fn main() -> impl Project {
593    ///     HelloProject
594    /// }
595    /// ```
596    #[must_use]
597    pub fn new() -> Self {
598        Self {}
599    }
600}
601
602impl App for DatabaseUserApp {
603    fn name(&self) -> &'static str {
604        "cot_db_user"
605    }
606
607    fn admin_model_managers(&self) -> Vec<Box<dyn AdminModelManager>> {
608        vec![Box::new(DefaultAdminModelManager::<DatabaseUser>::new())]
609    }
610
611    fn migrations(&self) -> Vec<Box<SyncDynMigration>> {
612        cot::db::migrations::wrap_migrations(migrations::MIGRATIONS)
613    }
614}
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619    use crate::config::SecretKey;
620    use crate::db::MockDatabaseBackend;
621
622    #[test]
623    #[cfg_attr(miri, ignore)]
624    fn session_auth_hash() {
625        let user = DatabaseUser::new(
626            Auto::fixed(1),
627            LimitedString::new("testuser").unwrap(),
628            &Password::new("password123"),
629        );
630        let secret_key = SecretKey::new(b"supersecretkey");
631
632        let hash = user.session_auth_hash(&secret_key);
633        assert!(hash.is_some());
634    }
635
636    #[test]
637    #[cfg_attr(miri, ignore)]
638    fn database_user_traits() {
639        let user = DatabaseUser::new(
640            Auto::fixed(1),
641            LimitedString::new("testuser").unwrap(),
642            &Password::new("password123"),
643        );
644        let user_ref: &dyn User = &user;
645        assert_eq!(user_ref.id(), Some(UserId::Int(1)));
646        assert_eq!(user_ref.username(), Some(Cow::from("testuser")));
647        assert!(user_ref.is_active());
648        assert!(user_ref.is_authenticated());
649        assert!(
650            user_ref
651                .session_auth_hash(&SecretKey::new(b"supersecretkey"))
652                .is_some()
653        );
654    }
655
656    #[cot::test]
657    #[cfg_attr(miri, ignore)]
658    async fn create_user() {
659        let mut mock_db = MockDatabaseBackend::new();
660        mock_db
661            .expect_insert::<DatabaseUser>()
662            .returning(|_| Ok(()));
663
664        let username = "testuser".to_string();
665        let password = Password::new("password123");
666
667        let user = DatabaseUser::create_user(&mock_db, username.clone(), &password)
668            .await
669            .unwrap();
670        assert_eq!(user.username(), username);
671    }
672
673    #[cot::test]
674    #[cfg_attr(miri, ignore)]
675    async fn get_by_id() {
676        let mut mock_db = MockDatabaseBackend::new();
677        let user = DatabaseUser::new(
678            Auto::fixed(1),
679            LimitedString::new("testuser").unwrap(),
680            &Password::new("password123"),
681        );
682
683        mock_db
684            .expect_get::<DatabaseUser>()
685            .returning(move |_| Ok(Some(user.clone())));
686
687        let result = DatabaseUser::get_by_id(&mock_db, 1).await.unwrap();
688        assert!(result.is_some());
689        assert_eq!(result.unwrap().username(), "testuser");
690    }
691
692    #[cot::test]
693    #[cfg_attr(miri, ignore)]
694    async fn authenticate() {
695        let mut mock_db = MockDatabaseBackend::new();
696        let user = DatabaseUser::new(
697            Auto::fixed(1),
698            LimitedString::new("testuser").unwrap(),
699            &Password::new("password123"),
700        );
701
702        mock_db
703            .expect_get::<DatabaseUser>()
704            .returning(move |_| Ok(Some(user.clone())));
705
706        let credentials =
707            DatabaseUserCredentials::new("testuser".to_string(), Password::new("password123"));
708        let result = DatabaseUser::authenticate(&mock_db, &credentials)
709            .await
710            .unwrap();
711        assert!(result.is_some());
712        assert_eq!(result.unwrap().username(), "testuser");
713    }
714
715    #[cot::test]
716    #[cfg_attr(miri, ignore)]
717    async fn authenticate_non_existing() {
718        let mut mock_db = MockDatabaseBackend::new();
719
720        mock_db
721            .expect_get::<DatabaseUser>()
722            .returning(move |_| Ok(None));
723
724        let credentials =
725            DatabaseUserCredentials::new("testuser".to_string(), Password::new("password123"));
726        let result = DatabaseUser::authenticate(&mock_db, &credentials)
727            .await
728            .unwrap();
729        assert!(result.is_none());
730    }
731
732    #[cot::test]
733    #[cfg_attr(miri, ignore)]
734    async fn authenticate_invalid_password() {
735        let mut mock_db = MockDatabaseBackend::new();
736        let user = DatabaseUser::new(
737            Auto::fixed(1),
738            LimitedString::new("testuser").unwrap(),
739            &Password::new("password123"),
740        );
741
742        mock_db
743            .expect_get::<DatabaseUser>()
744            .returning(move |_| Ok(Some(user.clone())));
745
746        let credentials =
747            DatabaseUserCredentials::new("testuser".to_string(), Password::new("invalid"));
748        let result = DatabaseUser::authenticate(&mock_db, &credentials)
749            .await
750            .unwrap();
751        assert!(result.is_none());
752    }
753}