Skip to main content

revolt_database/models/accounts/
model.rs

1use iso8601_timestamp::{Duration, Timestamp};
2
3use nanoid::nanoid;
4use revolt_config::config;
5use revolt_result::Result;
6use serde_json::json;
7
8use crate::{
9    events::client::EventV1,
10    util::{
11        email::{email_templates, normalise_email, send_email},
12        password::hash_password,
13    },
14    Database, MFATicket, Session,
15};
16use revolt_models::v0;
17
18auto_derived_partial!(
19    /// Account model
20    pub struct Account {
21        /// Unique Id
22        #[serde(rename = "_id")]
23        pub id: String,
24
25        /// User's email
26        pub email: String,
27
28        /// Normalised email
29        ///
30        /// (see https://github.com/insertish/authifier/#how-does-authifier-work)
31        pub email_normalised: String,
32
33        /// Argon2 hashed password
34        pub password: String,
35
36        /// Whether the account is disabled
37        #[serde(default)]
38        pub disabled: bool,
39
40        /// Email verification status
41        pub verification: EmailVerification,
42
43        /// Password reset information
44        pub password_reset: Option<PasswordReset>,
45
46        /// Account deletion information
47        pub deletion: Option<DeletionInfo>,
48
49        /// Account lockout
50        pub lockout: Option<Lockout>,
51
52        /// Multi-factor authentication information
53        pub mfa: MultiFactorAuthentication,
54    },
55    "PartialAccount"
56);
57
58auto_derived!(
59    /// Email verification status
60    #[serde(tag = "status")]
61    pub enum EmailVerification {
62        /// Account is verified
63        Verified,
64        /// Pending email verification
65        Pending { token: String, expiry: Timestamp },
66        /// Moving to a new email
67        Moving {
68            new_email: String,
69            token: String,
70            expiry: Timestamp,
71        },
72    }
73
74    /// Password reset information
75    pub struct PasswordReset {
76        /// Token required to change password
77        pub token: String,
78        /// Time at which this token expires
79        pub expiry: Timestamp,
80    }
81
82    /// Account deletion information
83    #[serde(tag = "status")]
84    pub enum DeletionInfo {
85        /// The user must confirm deletion by email
86        WaitingForVerification { token: String, expiry: Timestamp },
87        /// The account is scheduled for deletion
88        Scheduled { after: Timestamp },
89        /// This account was deleted
90        Deleted,
91    }
92
93    /// Lockout information
94    pub struct Lockout {
95        /// Attempt counter
96        pub attempts: i32,
97        /// Time at which this lockout expires
98        pub expiry: Option<Timestamp>,
99    }
100
101    /// MFA configuration
102    #[derive(Default)]
103    pub struct MultiFactorAuthentication {
104        /// Allow password-less email OTP login
105        /// (1-Factor)
106        // #[serde(skip_serializing_if = "is_false", default)]
107        // pub enable_email_otp: bool,
108
109        /// Allow trusted handover
110        /// (1-Factor)
111        // #[serde(skip_serializing_if = "is_false", default)]
112        // pub enable_trusted_handover: bool,
113
114        /// Allow email MFA
115        /// (2-Factor)
116        // #[serde(skip_serializing_if = "is_false", default)]
117        // pub enable_email_mfa: bool,
118
119        /// TOTP MFA token, enabled if present
120        /// (2-Factor)
121        #[serde(skip_serializing_if = "Totp::is_empty", default)]
122        pub totp_token: Totp,
123
124        /// Security Key MFA token, enabled if present
125        /// (2-Factor)
126        // #[serde(skip_serializing_if = "Option::is_none")]
127        // pub security_key_token: Option<String>,
128
129        /// Recovery codes
130        #[serde(skip_serializing_if = "Vec::is_empty", default)]
131        pub recovery_codes: Vec<String>,
132    }
133
134    /// MFA method
135    #[derive(Hash)]
136    pub enum MFAMethod {
137        Password,
138        Recovery,
139        Totp,
140    }
141
142    #[derive(Default)]
143    #[serde(tag = "status")]
144    pub enum Totp {
145        /// Disabled
146        #[default]
147        Disabled,
148        /// Waiting for user activation
149        Pending { secret: String },
150        /// Required on account
151        Enabled { secret: String },
152    }
153);
154
155impl MultiFactorAuthentication {
156    // Check whether MFA is in-use
157    pub fn is_active(&self) -> bool {
158        matches!(self.totp_token, Totp::Enabled { .. })
159    }
160
161    // Check whether there are still usable recovery codes
162    pub fn has_recovery(&self) -> bool {
163        !self.recovery_codes.is_empty()
164    }
165
166    // Get available MFA methods
167    pub fn get_methods(&self) -> Vec<MFAMethod> {
168        if let Totp::Enabled { .. } = self.totp_token {
169            let mut methods = vec![MFAMethod::Totp];
170
171            if self.has_recovery() {
172                methods.push(MFAMethod::Recovery);
173            }
174
175            methods
176        } else {
177            vec![MFAMethod::Password]
178        }
179    }
180
181    // Generate new recovery codes
182    pub fn generate_recovery_codes(&mut self) {
183        static ALPHABET: [char; 32] = [
184            '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
185            'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z',
186        ];
187
188        let mut codes = vec![];
189        for _ in 1..=10 {
190            codes.push(format!(
191                "{}-{}",
192                nanoid!(5, &ALPHABET),
193                nanoid!(5, &ALPHABET)
194            ));
195        }
196
197        self.recovery_codes = codes;
198    }
199
200    // Generate new TOTP secret
201    pub fn generate_new_totp_secret(&mut self) -> Result<String> {
202        if let Totp::Enabled { .. } = self.totp_token {
203            return Err(create_error!(OperationFailed));
204        }
205
206        let secret: [u8; 10] = rand::random();
207        let secret = base32::encode(base32::Alphabet::RFC4648 { padding: false }, &secret);
208
209        self.totp_token = Totp::Pending {
210            secret: secret.clone(),
211        };
212
213        Ok(secret)
214    }
215
216    /// Enable TOTP using a given MFA response
217    pub fn enable_totp(&mut self, response: v0::MFAResponse) -> Result<()> {
218        if let v0::MFAResponse::Totp { totp_code } = response {
219            let code = self.totp_token.generate_code()?;
220
221            if code == totp_code {
222                let mut totp = Totp::Disabled;
223                std::mem::swap(&mut totp, &mut self.totp_token);
224
225                if let Totp::Pending { secret } = totp {
226                    self.totp_token = Totp::Enabled { secret };
227
228                    Ok(())
229                } else {
230                    Err(create_error!(OperationFailed))
231                }
232            } else {
233                Err(create_error!(InvalidToken))
234            }
235        } else {
236            Err(create_error!(InvalidToken))
237        }
238    }
239}
240
241impl Totp {
242    /// Whether TOTP information is empty
243    pub fn is_empty(&self) -> bool {
244        matches!(self, Totp::Disabled)
245    }
246
247    /// Whether TOTP is disabled
248    pub fn is_disabled(&self) -> bool {
249        !matches!(self, Totp::Enabled { .. })
250    }
251
252    // Generate a TOTP code from secret
253    pub fn generate_code(&self) -> Result<String> {
254        if let Totp::Enabled { secret } | Totp::Pending { secret } = &self {
255            let seconds: u64 = std::time::SystemTime::now()
256                .duration_since(std::time::UNIX_EPOCH)
257                .unwrap()
258                .as_secs();
259
260            Ok(totp_lite::totp_custom::<totp_lite::Sha1>(
261                totp_lite::DEFAULT_STEP,
262                6,
263                &base32::decode(base32::Alphabet::RFC4648 { padding: false }, secret)
264                    .expect("valid base32 secret"),
265                seconds,
266            ))
267        } else {
268            Err(create_error!(OperationFailed))
269        }
270    }
271}
272
273impl Account {
274    /// Save model
275    pub async fn save(&self, db: &Database) -> Result<()> {
276        db.save_account(self).await
277    }
278
279    /// Create a new account
280    pub async fn new(
281        db: &Database,
282        email: String,
283        plaintext_password: String,
284        verify_email: bool,
285    ) -> Result<Account> {
286        // Get a normalised representation of the user's email
287        let email_normalised = normalise_email(email.clone());
288
289        // Try to find an existing account
290        if let Some(mut account) = db
291            .fetch_account_by_normalised_email(&email_normalised)
292            .await?
293        {
294            // Resend account verification or send password reset
295            if let EmailVerification::Pending { .. } = &account.verification {
296                account.start_email_verification(db).await?;
297            } else {
298                account.start_password_reset(db, true).await?;
299            }
300
301            Ok(account)
302        } else {
303            // Hash the user's password
304            let password = hash_password(plaintext_password)?;
305
306            // Create a new account
307            let mut account = Account {
308                id: ulid::Ulid::new().to_string(),
309
310                email,
311                email_normalised,
312                password,
313
314                disabled: false,
315                verification: EmailVerification::Verified,
316                password_reset: None,
317                deletion: None,
318                lockout: None,
319
320                mfa: Default::default(),
321            };
322
323            // Send email verification
324            if verify_email {
325                account.start_email_verification(db).await?;
326            } else {
327                account.save(db).await?;
328            }
329
330            // Create and push event
331            EventV1::CreateAccount {
332                account: account.clone(),
333            }
334            .global()
335            .await;
336
337            Ok(account)
338        }
339    }
340
341    /// Create a new session
342    pub async fn create_session(&self, db: &Database, name: String) -> Result<Session> {
343        let config = config().await;
344
345        let session = Session {
346            id: ulid::Ulid::new().to_string(),
347            token: nanoid!(64),
348
349            user_id: self.id.clone(),
350            name,
351
352            last_seen: Timestamp::now_utc(),
353
354            origin: Some(config.environment),
355            subscription: None,
356        };
357
358        // Save to database
359        db.save_session(&session).await?;
360
361        // Create and push event
362        EventV1::CreateSession {
363            session: session.clone(),
364        }
365        .global()
366        .await;
367
368        Ok(session)
369    }
370
371    /// Send account verification email
372    pub async fn start_email_verification(&mut self, db: &Database) -> Result<()> {
373        let config = config().await;
374
375        if !config.api.smtp.host.is_empty() {
376            let templates = email_templates().await;
377
378            let token = nanoid!(32);
379            let url = format!("{}{}", templates.verify.url, token);
380
381            send_email(
382                &config.api.smtp,
383                self.email.clone(),
384                &templates.verify,
385                json!({
386                    "email": self.email.clone(),
387                    "url": url
388                }),
389            )?;
390
391            self.verification = EmailVerification::Pending {
392                token,
393                expiry: Timestamp::now_utc()
394                    .checked_add(Duration::seconds(
395                        config.api.smtp.expiry.expire_verification,
396                    ))
397                    .unwrap(),
398            };
399        } else {
400            self.verification = EmailVerification::Verified;
401        }
402
403        self.save(db).await
404    }
405
406    /// Send account verification to new email
407    pub async fn start_email_move(&mut self, db: &Database, new_email: String) -> Result<()> {
408        // This method should and will never be called on an unverified account,
409        // but just validate this just in case.
410        if let EmailVerification::Pending { .. } = self.verification {
411            return Err(create_error!(UnverifiedAccount));
412        }
413
414        let config = config().await;
415
416        if !config.api.smtp.host.is_empty() {
417            let templates = email_templates().await;
418
419            let token = nanoid!(32);
420            let url = format!("{}{}", templates.verify.url, token);
421
422            send_email(
423                &config.api.smtp,
424                new_email.clone(),
425                &templates.verify,
426                json!({
427                    "email": self.email.clone(),
428                    "url": url
429                }),
430            )?;
431
432            self.verification = EmailVerification::Moving {
433                new_email,
434                token,
435                expiry: Timestamp::now_utc()
436                    .checked_add(Duration::seconds(
437                        config.api.smtp.expiry.expire_verification,
438                    ))
439                    .unwrap(),
440            };
441        } else {
442            self.email_normalised = normalise_email(new_email.clone());
443            self.email = new_email;
444        }
445
446        self.save(db).await
447    }
448
449    /// Send password reset email
450    pub async fn start_password_reset(
451        &mut self,
452        db: &Database,
453        existing_account: bool,
454    ) -> Result<()> {
455        let config = config().await;
456
457        if !config.api.smtp.host.is_empty() {
458            let templates = email_templates().await;
459
460            let template = if existing_account {
461                &templates.reset_existing
462            } else {
463                &templates.reset
464            };
465
466            let token = nanoid!(32);
467            let url = format!("{}{}", template.url, token);
468
469            send_email(
470                &config.api.smtp,
471                self.email.clone(),
472                template,
473                json!({
474                    "email": self.email.clone(),
475                    "url": url
476                }),
477            )?;
478
479            self.password_reset = Some(PasswordReset {
480                token,
481                expiry: Timestamp::now_utc()
482                    .checked_add(Duration::seconds(
483                        config.api.smtp.expiry.expire_password_reset,
484                    ))
485                    .unwrap(),
486            });
487        } else {
488            return Err(create_error!(OperationFailed));
489        }
490
491        self.save(db).await
492    }
493
494    /// Begin account deletion process by sending confirmation email
495    ///
496    /// If email verification is not on, the account will be marked for deletion instantly
497    pub async fn start_account_deletion(&mut self, db: &Database) -> Result<()> {
498        let config = config().await;
499
500        if !config.api.smtp.host.is_empty() {
501            let templates = email_templates().await;
502
503            let token = nanoid!(32);
504            let url = format!("{}{}", templates.deletion.url, token);
505
506            send_email(
507                &config.api.smtp,
508                self.email.clone(),
509                &templates.deletion,
510                json!({
511                    "email": self.email.clone(),
512                    "url": url
513                }),
514            )?;
515
516            self.deletion = Some(DeletionInfo::WaitingForVerification {
517                token,
518                expiry: Timestamp::now_utc()
519                    .checked_add(Duration::seconds(
520                        config.api.smtp.expiry.expire_password_reset,
521                    ))
522                    .unwrap(),
523            });
524
525            self.save(db).await
526        } else {
527            self.schedule_deletion(db).await
528        }
529    }
530
531    /// Verify a user's password is correct
532    pub fn verify_password(&self, plaintext_password: &str) -> Result<()> {
533        argon2::verify_encoded(&self.password, plaintext_password.as_bytes())
534            .map(|v| {
535                if v {
536                    Ok(())
537                } else {
538                    Err(create_error!(InvalidCredentials))
539                }
540            })
541            // To prevent user enumeration, we should ignore
542            // the error and pretend the password is wrong.
543            .map_err(|_| create_error!(InvalidCredentials))?
544    }
545
546    /// Validate an MFA response
547    pub async fn consume_mfa_response(
548        &mut self,
549        db: &Database,
550        response: v0::MFAResponse,
551        ticket: Option<MFATicket>,
552    ) -> Result<()> {
553        let allowed_methods = self.mfa.get_methods();
554
555        match response {
556            v0::MFAResponse::Password { password } => {
557                if allowed_methods.contains(&MFAMethod::Password) {
558                    self.verify_password(&password)
559                } else {
560                    Err(create_error!(DisallowedMFAMethod))
561                }
562            }
563            v0::MFAResponse::Totp { totp_code } => {
564                if allowed_methods.contains(&MFAMethod::Totp) {
565                    if let Totp::Enabled { .. } = &self.mfa.totp_token {
566                        // Use TOTP code at generation if applicable
567                        if let Some(ticket) = ticket {
568                            if let Some(code) = ticket.last_totp_code {
569                                if code == totp_code {
570                                    return Ok(());
571                                }
572                            }
573                        }
574
575                        // Otherwise read current TOTP token
576                        if self.mfa.totp_token.generate_code()? == totp_code {
577                            Ok(())
578                        } else {
579                            Err(create_error!(InvalidToken))
580                        }
581                    } else {
582                        unreachable!()
583                    }
584                } else {
585                    Err(create_error!(DisallowedMFAMethod))
586                }
587            }
588            v0::MFAResponse::Recovery { recovery_code } => {
589                if allowed_methods.contains(&MFAMethod::Recovery) {
590                    if let Some(index) = self
591                        .mfa
592                        .recovery_codes
593                        .iter()
594                        .position(|x| x == &recovery_code)
595                    {
596                        self.mfa.recovery_codes.remove(index);
597                        self.save(db).await
598                    } else {
599                        Err(create_error!(InvalidToken))
600                    }
601                } else {
602                    Err(create_error!(DisallowedMFAMethod))
603                }
604            }
605        }
606    }
607
608    /// Delete all sessions for an account
609    pub async fn delete_all_sessions(
610        &self,
611        db: &Database,
612        exclude_session_id: Option<String>,
613    ) -> Result<()> {
614        db.delete_all_sessions(&self.id, exclude_session_id.clone())
615            .await?;
616
617        // Create and push event
618        EventV1::DeleteAllSessions {
619            user_id: self.id.clone(),
620            exclude_session_id,
621        }
622        .private(self.id.clone())
623        .await;
624
625        Ok(())
626    }
627
628    /// Disable an account
629    pub async fn disable(&mut self, db: &Database) -> Result<()> {
630        self.disabled = true;
631        self.delete_all_sessions(db, None).await?;
632        self.save(db).await
633    }
634
635    /// Schedule an account for deletion
636    pub async fn schedule_deletion(&mut self, db: &Database) -> Result<()> {
637        self.deletion = Some(DeletionInfo::Scheduled {
638            after: Timestamp::now_utc()
639                .checked_add(Duration::weeks(1))
640                .unwrap(),
641        });
642
643        self.disable(db).await
644    }
645
646    /// Removes all information from the account and marks it as fully deleted
647    pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
648        self.email = format!("Deleted User {}", &self.id);
649        self.email_normalised = format!("Deleted User {}", &self.id);
650        self.deletion = Some(DeletionInfo::Deleted);
651
652        self.save(db).await?;
653
654        Ok(())
655    }
656}