Skip to main content

revolt_database/models/mfa_tickets/
model.rs

1use iso8601_timestamp::{Duration, Timestamp};
2use std::ops::Deref;
3
4use nanoid::nanoid;
5use revolt_result::Result;
6
7use crate::{Database, MultiFactorAuthentication};
8
9auto_derived_partial!(
10    /// Multi-factor auth ticket
11    pub struct MFATicket {
12        /// Unique Id
13        #[serde(rename = "_id")]
14        pub id: String,
15
16        /// Account Id
17        pub account_id: String,
18
19        /// Unique Token
20        pub token: String,
21
22        /// Whether this ticket has been validated
23        /// (can be used for account actions)
24        pub validated: bool,
25
26        /// Whether this ticket is authorised
27        /// (can be used to log a user in)
28        pub authorised: bool,
29
30        /// TOTP code at time of ticket creation
31        pub last_totp_code: Option<String>,
32    },
33    "PartialMFATicket"
34);
35
36/// Ticket which is guaranteed to be valid for use
37///
38/// If used in a Rocket guard, it will be consumed on match
39#[derive(Debug, Serialize, Deserialize)]
40pub struct ValidatedTicket(pub MFATicket);
41
42/// Ticket which is guaranteed to not be valid for use
43#[derive(Debug, Serialize, Deserialize)]
44pub struct UnvalidatedTicket(pub MFATicket);
45
46impl MFATicket {
47    /// Create a new MFA ticket
48    pub fn new(account_id: String, validated: bool) -> MFATicket {
49        MFATicket {
50            id: ulid::Ulid::new().to_string(),
51            account_id,
52            token: nanoid!(64),
53            validated,
54            authorised: false,
55            last_totp_code: None,
56        }
57    }
58
59    /// Populate an MFA ticket with valid MFA codes
60    pub async fn populate(&mut self, mfa: &MultiFactorAuthentication) {
61        self.last_totp_code = mfa.totp_token.generate_code().ok();
62    }
63
64    /// Save model
65    pub async fn save(&self, db: &Database) -> Result<()> {
66        db.save_ticket(self).await
67    }
68
69    /// Check if this MFA ticket has expired
70    pub fn is_expired(&self) -> bool {
71        let now = Timestamp::now_utc();
72
73        let datetime: Timestamp = ulid::Ulid::from_string(&self.id)
74            .expect("Valid `ulid`")
75            .datetime()
76            .into();
77
78        now > (datetime.checked_add(Duration::minutes(5)).unwrap())
79    }
80
81    /// Claim and remove this MFA ticket
82    pub async fn claim(&self, db: &Database) -> Result<()> {
83        if self.is_expired() {
84            return Err(create_error!(InvalidToken));
85        }
86
87        db.delete_ticket(&self.id).await
88    }
89}
90
91impl Deref for ValidatedTicket {
92    type Target = MFATicket;
93
94    fn deref(&self) -> &Self::Target {
95        &self.0
96    }
97}
98
99impl Deref for UnvalidatedTicket {
100    type Target = MFATicket;
101
102    fn deref(&self) -> &Self::Target {
103        &self.0
104    }
105}