Skip to main content

authifier/impl/
account.rs

1use chrono::Duration;
2use iso8601_timestamp::Timestamp;
3
4use crate::{
5    config::EmailVerificationConfig,
6    models::{
7        totp::Totp, Account, DeletionInfo, EmailVerification, MFAMethod, MFAResponse, MFATicket,
8        PasswordReset, Session,
9    },
10    util::{hash_password, normalise_email},
11    Authifier, AuthifierEvent, Error, Result, Success,
12};
13
14impl Account {
15    /// Save model
16    pub async fn save(&self, authifier: &Authifier) -> Success {
17        authifier.database.save_account(self).await
18    }
19
20    /// Create a new account
21    pub async fn new(
22        authifier: &Authifier,
23        email: String,
24        plaintext_password: String,
25        verify_email: bool,
26    ) -> Result<Account> {
27        // Hash the user's password
28        let password = hash_password(plaintext_password)?;
29
30        // Get a normalised representation of the user's email
31        let email_normalised = normalise_email(email.clone());
32
33        // Try to find an existing account
34        if let Some(mut account) = authifier
35            .database
36            .find_account_by_normalised_email(&email_normalised)
37            .await?
38        {
39            // Resend account verification or send password reset
40            if let EmailVerification::Pending { .. } = &account.verification {
41                account.start_email_verification(authifier).await?;
42            } else {
43                account.start_password_reset(authifier, true).await?;
44            }
45
46            Ok(account)
47        } else {
48            // Create a new account
49            let mut account = Account {
50                id: ulid::Ulid::new().to_string(),
51
52                email,
53                email_normalised,
54                password,
55
56                disabled: false,
57                verification: EmailVerification::Verified,
58                password_reset: None,
59                deletion: None,
60                lockout: None,
61
62                mfa: Default::default(),
63            };
64
65            // Send email verification
66            if verify_email {
67                account.start_email_verification(authifier).await?;
68            } else {
69                account.save(authifier).await?;
70            }
71
72            // Create and push event
73            authifier
74                .publish_event(AuthifierEvent::CreateAccount {
75                    account: account.clone(),
76                })
77                .await;
78
79            Ok(account)
80        }
81    }
82
83    /// Create a new session
84    pub async fn create_session(&self, authifier: &Authifier, name: String) -> Result<Session> {
85        let session = Session {
86            id: ulid::Ulid::new().to_string(),
87            token: nanoid!(64),
88
89            user_id: self.id.clone(),
90            name,
91
92            last_seen: Timestamp::now_utc().format().to_string(),
93
94            origin: None,
95            subscription: None,
96        };
97
98        // Save to database
99        authifier.database.save_session(&session).await?;
100
101        // Create and push event
102        authifier
103            .publish_event(AuthifierEvent::CreateSession {
104                session: session.clone(),
105            })
106            .await;
107
108        Ok(session)
109    }
110
111    /// Send account verification email
112    pub async fn start_email_verification(&mut self, authifier: &Authifier) -> Success {
113        if let EmailVerificationConfig::Enabled {
114            templates,
115            expiry,
116            smtp,
117        } = &authifier.config.email_verification
118        {
119            let token = nanoid!(32);
120            let url = format!("{}{}", templates.verify.url, token);
121
122            smtp.send_email(
123                self.email.clone(),
124                &templates.verify,
125                json!({
126                    "email": self.email.clone(),
127                    "url": url
128                }),
129            )?;
130
131            self.verification = EmailVerification::Pending {
132                token,
133                expiry: Timestamp::UNIX_EPOCH
134                    + iso8601_timestamp::Duration::milliseconds(
135                        (chrono::Utc::now() + Duration::seconds(expiry.expire_verification))
136                            .timestamp_millis(),
137                    ),
138            };
139        } else {
140            self.verification = EmailVerification::Verified;
141        }
142
143        self.save(authifier).await
144    }
145
146    /// Send account verification to new email
147    pub async fn start_email_move(&mut self, authifier: &Authifier, new_email: String) -> Success {
148        // This method should and will never be called on an unverified account,
149        // but just validate this just in case.
150        if let EmailVerification::Pending { .. } = self.verification {
151            return Err(Error::UnverifiedAccount);
152        }
153
154        if let EmailVerificationConfig::Enabled {
155            templates,
156            expiry,
157            smtp,
158        } = &authifier.config.email_verification
159        {
160            let token = nanoid!(32);
161            let url = format!("{}{}", templates.verify.url, token);
162
163            smtp.send_email(
164                new_email.clone(),
165                &templates.verify,
166                json!({
167                    "email": self.email.clone(),
168                    "url": url
169                }),
170            )?;
171
172            self.verification = EmailVerification::Moving {
173                new_email,
174                token,
175                expiry: Timestamp::UNIX_EPOCH
176                    + iso8601_timestamp::Duration::milliseconds(
177                        (chrono::Utc::now() + Duration::seconds(expiry.expire_verification))
178                            .timestamp_millis(),
179                    ),
180            };
181        } else {
182            self.email_normalised = normalise_email(new_email.clone());
183            self.email = new_email;
184        }
185
186        self.save(authifier).await
187    }
188
189    /// Send password reset email
190    pub async fn start_password_reset(
191        &mut self,
192        authifier: &Authifier,
193        existing_account: bool,
194    ) -> Success {
195        if let EmailVerificationConfig::Enabled {
196            templates,
197            expiry,
198            smtp,
199        } = &authifier.config.email_verification
200        {
201            let template = if existing_account {
202                &templates.reset_existing
203            } else {
204                &templates.reset
205            };
206
207            let token = nanoid!(32);
208            let url = format!("{}{}", template.url, token);
209
210            smtp.send_email(
211                self.email.clone(),
212                &template,
213                json!({
214                    "email": self.email.clone(),
215                    "url": url
216                }),
217            )?;
218
219            self.password_reset = Some(PasswordReset {
220                token,
221                expiry: Timestamp::UNIX_EPOCH
222                    + iso8601_timestamp::Duration::milliseconds(
223                        (chrono::Utc::now() + Duration::seconds(expiry.expire_password_reset))
224                            .timestamp_millis(),
225                    ),
226            });
227        } else {
228            return Err(Error::OperationFailed);
229        }
230
231        self.save(authifier).await
232    }
233
234    /// Begin account deletion process by sending confirmation email
235    ///
236    /// If email verification is not on, the account will be marked for deletion instantly
237    pub async fn start_account_deletion(&mut self, authifier: &Authifier) -> Success {
238        if let EmailVerificationConfig::Enabled {
239            templates,
240            expiry,
241            smtp,
242        } = &authifier.config.email_verification
243        {
244            let token = nanoid!(32);
245            let url = format!("{}{}", templates.deletion.url, token);
246
247            smtp.send_email(
248                self.email.clone(),
249                &templates.deletion,
250                json!({
251                    "email": self.email.clone(),
252                    "url": url
253                }),
254            )?;
255
256            self.deletion = Some(DeletionInfo::WaitingForVerification {
257                token,
258                expiry: Timestamp::UNIX_EPOCH
259                    + iso8601_timestamp::Duration::milliseconds(
260                        (chrono::Utc::now() + Duration::seconds(expiry.expire_password_reset))
261                            .timestamp_millis(),
262                    ),
263            });
264
265            self.save(authifier).await
266        } else {
267            self.schedule_deletion(authifier).await
268        }
269    }
270
271    /// Verify a user's password is correct
272    pub fn verify_password(&self, plaintext_password: &str) -> Success {
273        argon2::verify_encoded(&self.password, plaintext_password.as_bytes())
274            .map(|v| {
275                if v {
276                    Ok(())
277                } else {
278                    Err(Error::InvalidCredentials)
279                }
280            })
281            // To prevent user enumeration, we should ignore
282            // the error and pretend the password is wrong.
283            .map_err(|_| Error::InvalidCredentials)?
284    }
285
286    /// Validate an MFA response
287    pub async fn consume_mfa_response(
288        &mut self,
289        authifier: &Authifier,
290        response: MFAResponse,
291        ticket: Option<MFATicket>,
292    ) -> Success {
293        let allowed_methods = self.mfa.get_methods();
294
295        match response {
296            MFAResponse::Password { password } => {
297                if allowed_methods.contains(&MFAMethod::Password) {
298                    self.verify_password(&password)
299                } else {
300                    Err(Error::DisallowedMFAMethod)
301                }
302            }
303            MFAResponse::Totp { totp_code } => {
304                if allowed_methods.contains(&MFAMethod::Totp) {
305                    if let Totp::Enabled { .. } = &self.mfa.totp_token {
306                        // Use TOTP code at generation if applicable
307                        if let Some(ticket) = ticket {
308                            if let Some(code) = ticket.last_totp_code {
309                                if code == totp_code {
310                                    return Ok(());
311                                }
312                            }
313                        }
314
315                        // Otherwise read current TOTP token
316                        if self.mfa.totp_token.generate_code()? == totp_code {
317                            Ok(())
318                        } else {
319                            Err(Error::InvalidToken)
320                        }
321                    } else {
322                        unreachable!()
323                    }
324                } else {
325                    Err(Error::DisallowedMFAMethod)
326                }
327            }
328            MFAResponse::Recovery { recovery_code } => {
329                if allowed_methods.contains(&MFAMethod::Recovery) {
330                    if let Some(index) = self
331                        .mfa
332                        .recovery_codes
333                        .iter()
334                        .position(|x| x == &recovery_code)
335                    {
336                        self.mfa.recovery_codes.remove(index);
337                        self.save(authifier).await
338                    } else {
339                        Err(Error::InvalidToken)
340                    }
341                } else {
342                    Err(Error::DisallowedMFAMethod)
343                }
344            }
345        }
346    }
347
348    /// Delete all sessions for an account
349    pub async fn delete_all_sessions(
350        &self,
351        authifier: &Authifier,
352        exclude_session_id: Option<String>,
353    ) -> Success {
354        authifier
355            .database
356            .delete_all_sessions(&self.id, exclude_session_id.clone())
357            .await?;
358
359        // Create and push event
360        authifier
361            .publish_event(AuthifierEvent::DeleteAllSessions {
362                user_id: self.id.to_string(),
363                exclude_session_id,
364            })
365            .await;
366
367        Ok(())
368    }
369
370    /// Disable an account
371    pub async fn disable(&mut self, authifier: &Authifier) -> Success {
372        self.disabled = true;
373        self.delete_all_sessions(authifier, None).await?;
374        self.save(authifier).await
375    }
376
377    /// Schedule an account for deletion
378    pub async fn schedule_deletion(&mut self, authifier: &Authifier) -> Success {
379        self.deletion = Some(DeletionInfo::Scheduled {
380            after: Timestamp::UNIX_EPOCH
381                + iso8601_timestamp::Duration::milliseconds(
382                    (chrono::Utc::now() + Duration::weeks(1)).timestamp_millis(),
383                ),
384        });
385
386        self.disable(authifier).await
387    }
388}