Skip to main content

authlogic/
users.rs

1#[cfg(feature = "diesel")]
2use diesel::{prelude::*, sql_types::*};
3
4use std::fmt::Display;
5
6use crate::{
7    app::{App, AppTypes},
8    errors::Error,
9    hashing,
10    mail::{Challenge, Notification, issue_challenge},
11    maybe_auth::Auth,
12    secret::{PasswordHash, Secret},
13};
14
15pub trait UserID<T> {
16    /// Gets the user's id field.
17    fn id(&self) -> T;
18
19    /// Sets the user's id field. This is only called after inserting a new
20    /// unverified user, since that is when the user receives their unique id.
21    fn set_id(&mut self, new_id: T);
22    
23    /// Gets the user's identifier (e.g. username or email).
24    fn identifier(&self) -> &str;
25}
26
27#[derive(Debug, Clone, Copy)]
28#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow, sqlx::Type))]
29#[cfg_attr(feature = "diesel", derive(QueryableByName))]
30pub struct UserState {
31    #[cfg_attr(feature = "diesel", diesel(sql_type = Bool))]
32    pub has_password: bool,
33
34    #[cfg_attr(feature = "diesel", diesel(sql_type = Bool))]
35    pub is_suspended: bool,
36
37    #[cfg_attr(feature = "diesel", diesel(sql_type = Bool))]
38    pub require_email_verification: bool,
39    
40    #[cfg_attr(feature = "diesel", diesel(sql_type = Bool))]
41    pub require_password_change: bool,
42}
43
44impl UserState {
45    /// A user is "ready" if they are not suspended, and are not required to
46    /// perform some action (email verification or password change) before
47    /// continuing.
48    pub fn is_ready(self) -> bool {
49        !self.is_suspended && !self.require_email_verification && !self.require_password_change
50    }
51    
52    /// Requires that the user is "ready", or otherwise logs a message and
53    /// returns an error.
54    pub(crate) fn require_ready<ID: Display>(&self, id: ID) -> Result<(), Error> {
55        if self.is_suspended {
56            log::debug!("User {id} is suspended");
57            Err(Error::UserIsSuspended)
58        } else if self.require_email_verification {
59            log::debug!("User {id} requires email verification");
60            Err(Error::EmailNotVerified)
61        } else if self.require_password_change {
62            log::debug!("User {id} requires password change");
63            Err(Error::RequirePasswordChange)
64        } else {
65            Ok(())
66        }
67    }
68}
69
70impl Display for UserState {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        let mut any = false;
73        
74        if self.is_suspended {
75            f.write_str("suspended")?;
76            any = true;
77        }
78        
79        if self.require_email_verification {
80            if any { f.write_str(", ")?; }
81            f.write_str("unverified")?;
82            any = true;
83        }
84        
85        if self.require_password_change {
86            if any { f.write_str(", ")?; }
87            f.write_str("must change password")?;
88            any = true;
89        }
90        
91        if !any {
92            f.write_str("ready")?;
93        }
94        
95        Ok(())
96    }
97}
98
99#[cfg_attr(feature = "diesel", derive(QueryableByName))]
100pub struct UserData<A: AppTypes> {
101    #[cfg_attr(feature = "diesel", diesel(embed))]
102    pub user: A::User,
103    
104    #[cfg_attr(feature = "diesel", diesel(deserialize_as = Option<String>), diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>))]
105    pub password_hash: PasswordHash,
106    
107    #[cfg_attr(feature = "diesel", diesel(embed))]
108    pub state: UserState,
109}
110
111impl <A: App> UserData<A> {
112    /// Indicates whether there is a password associated with this user
113    /// account.
114    pub fn has_password(&self) -> bool {
115        self.password_hash.exists()
116    }
117}
118
119/// Registers a new user with a password they have chosen for themselves. An
120/// email verification challenge is sent, and the user must complete this
121/// before they can use their account.
122/// 
123/// Returns the registered user with their unique id.
124pub async fn register_new_user<A: App>(
125    app: &mut A,
126    user: A::User,
127    password: Secret,
128) -> Result<RegistrationOutcome<A>, A::Error> {
129    let len = password.expose().len();
130    let hash = hashing::generate_password_hash(&password)?;
131    
132    register(app, user, Some(len), None, hash)
133        .await
134}
135
136/// Registers a new user without a password. An email verification challenge is
137/// sent, and the user must complete this before they can use their account.
138/// After this, the user can log in by completing further email challenges.
139/// 
140/// Returns the registered user with their unique id.
141pub async fn register_new_user_without_password<A: App>(
142    app: &mut A,
143    user: A::User,
144) -> Result<RegistrationOutcome<A>, A::Error> {
145    register(app, user, None, None, PasswordHash::NONE)
146        .await
147}
148
149/// Registers a new user with a temporary password. Instead of an email
150/// verification challenge, the user is sent an email notification with the
151/// temporary password. When they first login, they will be required to choose
152/// a new password.
153/// 
154/// Returns the registered user with their unique id.
155pub async fn register_new_user_with_temporary_password<A: App>(
156    app: &mut A,
157    user: A::User,
158) -> Result<RegistrationOutcome<A>, A::Error> {
159    let (password, hash) = hashing::generate_password_and_hash()?;
160    
161    register(app, user, None, Some(password), hash)
162        .await
163}
164
165pub enum PasswordChangeOutcome {
166    /// Indicates that the password was changed successfully.
167    Success,
168    
169    /// Indicates that the user did not provide a correct current password.
170    IncorrectPassword,
171    
172    /// Indicates that the user chose a password which is shorter than
173    /// `AppConfig::minimum_password_length()`.
174    NewPasswordTooShort,
175    
176    /// Indicates that the user chose a new password which is the same as the
177    /// old one.
178    PasswordsNotDifferent,
179}
180
181pub async fn change_password<A: App>(
182    app: &mut A,
183    auth: Auth<A>,
184    old_password: Option<Secret>,
185    new_password: Secret,
186) -> Result<PasswordChangeOutcome, A::Error> {
187    let user = auth.user;
188
189    // Make sure they actually changed their password. This doesn't need to be
190    // done in constant-time, because both are provided by the user.
191    if matches!(&old_password, Some(old) if old.0 == new_password.0) {
192        return Ok(PasswordChangeOutcome::PasswordsNotDifferent);
193    }
194
195    // Make sure the new password is strong enough.
196    if new_password.0.len() < app.minimum_password_length() {
197        return Ok(PasswordChangeOutcome::NewPasswordTooShort);
198    }
199
200    // Verify the old password.
201    let data = app
202        .get_user_data_by_id(user.id())
203        .await
204        .map_err(Into::into)?
205        .ok_or(Error::UserDataQueryFailed {user_id: user.id().into()})?;
206    
207    // If the account is password-protected, verify the old password
208    if data.password_hash.exists() {
209        if let Some(old_password) = old_password {
210            // Account is password-protected, and old password is provided
211            let result = hashing::check_password(&data.password_hash, &old_password)?;
212            if !result {
213                return Ok(PasswordChangeOutcome::IncorrectPassword);
214            }
215        } else {
216            // Account is password-protected, but no old password is provided
217            return Ok(PasswordChangeOutcome::IncorrectPassword);
218        }
219    }
220
221    // Update the password in the database.
222    let new_hash = hashing::generate_password_hash(&new_password)?;
223    app.update_password(&user, new_hash, false)
224        .await
225        .map_err(Into::into)?;
226
227    // Notify the user that their password has been changed, in case they
228    // didn't change it themselves.
229    app.send_notification(&user, Notification::PasswordChanged)
230        .await
231        .map_err(Into::into)?;
232
233    Ok(PasswordChangeOutcome::Success)
234}
235
236pub async fn request_password_reset<A: App>(app: &mut A, user: &A::User) -> Result<(), A::Error> {
237    issue_challenge(app, user, Challenge::ResetPassword)
238        .await
239}
240
241pub enum RegistrationOutcome<A: App> {
242    /// Indicates that the user was registered successfully.
243    Success(A::User),
244    
245    /// Indicates that the user's identifier (e.g. username or email) already
246    /// belongs to an existing user.
247    IdentifierAlreadyExists,
248    
249    /// Indicates that the user chose a password which is shorter than
250    /// `AppConfig::minimum_password_length()`.
251    PasswordTooShort,
252}
253
254async fn register<A: App>(
255    app: &mut A,
256    mut user: A::User,
257    chosen_password_length: Option<usize>,
258    temporary_password: Option<Secret>,
259    password_hash: PasswordHash,
260) -> Result<RegistrationOutcome<A>, A::Error> {
261    let result = app.user_identifier_exists(user.identifier())
262        .await
263        .map_err(Into::into)?;
264    if result {
265        return Ok(RegistrationOutcome::IdentifierAlreadyExists);
266    }
267    
268    if matches!(chosen_password_length, Some(n) if n < app.minimum_password_length()) {
269        return Ok(RegistrationOutcome::PasswordTooShort);
270    }
271    
272    // Insert the user into the database. This has to be done first to get the
273    // user's new unique id, which might be needed by the app mailer.
274    let user_data = UserData {
275        user: user.clone(),
276        state: UserState {
277            has_password: password_hash.exists(),
278            is_suspended: false,
279            require_password_change: temporary_password.is_some(),
280            require_email_verification: temporary_password.is_none(),
281        },
282        password_hash,
283    };
284    let user_id = app.insert_user(user_data)
285        .await
286        .map_err(Into::into)?;
287
288    // Update the user's id.
289    user.set_id(user_id);
290
291    // Send the notification or challenge email.
292    let result = match temporary_password {
293        Some(temporary_password) => {
294            let notification = Notification::UserRegistered {temporary_password};
295            app.send_notification(&user, notification)
296                .await
297                .map_err(Into::into)
298        },
299        None => {
300            issue_challenge(app, &user, Challenge::VerifyNewUser)
301                .await
302                .map_err(Into::into)
303        }
304    };
305
306    if let Err(e) = result {
307        // If sending the email fails, the challenge code or temporary password
308        // are lost, and the user will never be able to log in.
309        app.delete_user(user_id)
310            .await
311            .map_err(Into::into)?;
312
313        return Err(e);
314    }
315
316    Ok(RegistrationOutcome::Success(user))
317}