cyaxon_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, normalize_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 normalized representation of the user's email
31        let email_normalized = normalize_email(email.clone());
32
33        // Try to find an existing account
34        if let Some(mut account) = authifier
35            .database
36            .find_account_by_normalized_email(&email_normalized)
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).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_normalized,
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            subscription: None,
93        };
94
95        // Save to database
96        authifier.database.save_session(&session).await?;
97
98        // Create and push event
99        authifier
100            .publish_event(AuthifierEvent::CreateSession {
101                session: session.clone(),
102            })
103            .await;
104
105        Ok(session)
106    }
107
108    /// Send account verification email
109    pub async fn start_email_verification(&mut self, authifier: &Authifier) -> Success {
110        if let EmailVerificationConfig::Enabled {
111            templates,
112            expiry,
113            smtp,
114        } = &authifier.config.email_verification
115        {
116            let token = nanoid!(32);
117            let url = format!("{}{}", templates.verify.url, token);
118
119            smtp.send_email(
120                self.email.clone(),
121                &templates.verify,
122                json!({
123                    "email": self.email.clone(),
124                    "url": url
125                }),
126            )?;
127
128            self.verification = EmailVerification::Pending {
129                token,
130                expiry: Timestamp::from_unix_timestamp_ms(
131                    chrono::Utc::now()
132                        .checked_add_signed(Duration::seconds(expiry.expire_verification))
133                        .expect("failed to checked_add_signed")
134                        .timestamp_millis(),
135                ),
136            };
137        } else {
138            self.verification = EmailVerification::Verified;
139        }
140
141        self.save(authifier).await
142    }
143
144    /// Send account verification to new email
145    pub async fn start_email_move(&mut self, authifier: &Authifier, new_email: String) -> Success {
146        // This method should and will never be called on an unverified account,
147        // but just validate this just in case.
148        if let EmailVerification::Pending { .. } = self.verification {
149            return Err(Error::UnverifiedAccount);
150        }
151
152        if let EmailVerificationConfig::Enabled {
153            templates,
154            expiry,
155            smtp,
156        } = &authifier.config.email_verification
157        {
158            let token = nanoid!(32);
159            let url = format!("{}{}", templates.verify.url, token);
160
161            smtp.send_email(
162                new_email.clone(),
163                &templates.verify,
164                json!({
165                    "email": self.email.clone(),
166                    "url": url
167                }),
168            )?;
169
170            self.verification = EmailVerification::Moving {
171                new_email,
172                token,
173                expiry: Timestamp::from_unix_timestamp_ms(
174                    chrono::Utc::now()
175                        .checked_add_signed(Duration::seconds(expiry.expire_verification))
176                        .expect("failed to checked_add_signed")
177                        .timestamp_millis(),
178                ),
179            };
180        } else {
181            self.email_normalized = normalize_email(new_email.clone());
182            self.email = new_email;
183        }
184
185        self.save(authifier).await
186    }
187
188    /// Send password reset email
189    pub async fn start_password_reset(&mut self, authifier: &Authifier) -> Success {
190        if let EmailVerificationConfig::Enabled {
191            templates,
192            expiry,
193            smtp,
194        } = &authifier.config.email_verification
195        {
196            let token = nanoid!(32);
197            let url = format!("{}{}", templates.reset.url, token);
198
199            smtp.send_email(
200                self.email.clone(),
201                &templates.reset,
202                json!({
203                    "email": self.email.clone(),
204                    "url": url
205                }),
206            )?;
207
208            self.password_reset = Some(PasswordReset {
209                token,
210                expiry: Timestamp::from_unix_timestamp_ms(
211                    chrono::Utc::now()
212                        .checked_add_signed(Duration::seconds(expiry.expire_password_reset))
213                        .expect("failed to checked_add_signed")
214                        .timestamp_millis(),
215                ),
216            });
217        } else {
218            return Err(Error::OperationFailed);
219        }
220
221        self.save(authifier).await
222    }
223
224    /// Begin account deletion process by sending confirmation email
225    ///
226    /// If email verification is not on, the account will be marked for deletion instantly
227    pub async fn start_account_deletion(&mut self, authifier: &Authifier) -> Success {
228        if let EmailVerificationConfig::Enabled {
229            templates,
230            expiry,
231            smtp,
232        } = &authifier.config.email_verification
233        {
234            let token = nanoid!(32);
235            let url = format!("{}{}", templates.deletion.url, token);
236
237            smtp.send_email(
238                self.email.clone(),
239                &templates.deletion,
240                json!({
241                    "email": self.email.clone(),
242                    "url": url
243                }),
244            )?;
245
246            self.deletion = Some(DeletionInfo::WaitingForVerification {
247                token,
248                expiry: Timestamp::from_unix_timestamp_ms(
249                    chrono::Utc::now()
250                        .checked_add_signed(Duration::seconds(expiry.expire_password_reset))
251                        .expect("failed to checked_add_signed")
252                        .timestamp_millis(),
253                ),
254            });
255
256            self.save(authifier).await
257        } else {
258            self.schedule_deletion(authifier).await
259        }
260    }
261
262    /// Verify a user's password is correct
263    pub fn verify_password(&self, plaintext_password: &str) -> Success {
264        bcrypt::verify(plaintext_password, &self.password)
265					.map(|v| {
266						if v {
267							Ok(())
268						} else {
269							Err(Error::InvalidCredentials)
270						}
271					})
272					// To prevent user enumeration, we should ignore
273					// the error and pretend the password is wrong.
274					.map_err(|_| Error::InvalidCredentials)?
275    }
276
277    /// Validate an MFA response
278    pub async fn consume_mfa_response(
279        &mut self,
280        authifier: &Authifier,
281        response: MFAResponse,
282        ticket: Option<MFATicket>,
283    ) -> Success {
284        let allowed_methods = self.mfa.get_methods();
285
286        match response {
287            MFAResponse::Password { password } => {
288                if allowed_methods.contains(&MFAMethod::Password) {
289                    self.verify_password(&password)
290                } else {
291                    Err(Error::DisallowedMFAMethod)
292                }
293            }
294            MFAResponse::Totp { totp_code } => {
295                if allowed_methods.contains(&MFAMethod::Totp) {
296                    if let Totp::Enabled { .. } = &self.mfa.totp_token {
297                        // Use TOTP code at generation if applicable
298                        if let Some(ticket) = ticket {
299                            if let Some(code) = ticket.last_totp_code {
300                                if code == totp_code {
301                                    return Ok(());
302                                }
303                            }
304                        }
305
306                        // Otherwise read current TOTP token
307                        if self.mfa.totp_token.generate_code()? == totp_code {
308                            Ok(())
309                        } else {
310                            Err(Error::InvalidToken)
311                        }
312                    } else {
313                        unreachable!()
314                    }
315                } else {
316                    Err(Error::DisallowedMFAMethod)
317                }
318            }
319            MFAResponse::Recovery { recovery_code } => {
320                if allowed_methods.contains(&MFAMethod::Recovery) {
321                    if let Some(index) = self
322                        .mfa
323                        .recovery_codes
324                        .iter()
325                        .position(|x| x == &recovery_code)
326                    {
327                        self.mfa.recovery_codes.remove(index);
328                        self.save(authifier).await
329                    } else {
330                        Err(Error::InvalidToken)
331                    }
332                } else {
333                    Err(Error::DisallowedMFAMethod)
334                }
335            }
336        }
337    }
338
339    /// Delete all sessions for an account
340    pub async fn delete_all_sessions(
341        &self,
342        authifier: &Authifier,
343        exclude_session_id: Option<String>,
344    ) -> Success {
345        authifier
346            .database
347            .delete_all_sessions(&self.id, exclude_session_id.clone())
348            .await?;
349
350        // Create and push event
351        authifier
352            .publish_event(AuthifierEvent::DeleteAllSessions {
353                user_id: self.id.to_string(),
354                exclude_session_id,
355            })
356            .await;
357
358        Ok(())
359    }
360
361    /// Disable an account
362    pub async fn disable(&mut self, authifier: &Authifier) -> Success {
363        self.disabled = true;
364        self.delete_all_sessions(authifier, None).await?;
365        self.save(authifier).await
366    }
367
368    /// Schedule an account for deletion
369    pub async fn schedule_deletion(&mut self, authifier: &Authifier) -> Success {
370        self.deletion = Some(DeletionInfo::Scheduled {
371            after: Timestamp::from_unix_timestamp_ms(
372                chrono::Utc::now()
373                    .checked_add_signed(Duration::weeks(1))
374                    .expect("failed to checked_add_signed")
375                    .timestamp_millis(),
376            ),
377        });
378
379        self.disable(authifier).await
380    }
381}