authlogic/
users.rs

1use std::fmt::Display;
2
3use crate::{
4    app::{App, AppTypes},
5    errors::Error,
6    hashing,
7    mail::{Challenge, Notification, issue_challenge},
8    maybe_auth::Auth,
9    secret::{PasswordHash, Secret},
10};
11
12pub trait UserID<T> {
13    /// Gets the user's id field.
14    fn id(&self) -> T;
15
16    /// Sets the user's id field. This is only called after inserting a new
17    /// unverified user, since that is when the user receives their unique id.
18    fn set_id(&mut self, new_id: T);
19}
20
21#[cfg_attr(feature = "diesel", derive(diesel::prelude::QueryableByName))]
22pub struct UserData<A: AppTypes> {
23    #[cfg_attr(feature = "diesel", diesel(embed))]
24    pub user: A::User,
25    
26    #[cfg_attr(feature = "diesel", diesel(deserialize_as = String), diesel(sql_type = diesel::sql_types::Text))]
27    pub password_hash: PasswordHash,
28    
29    #[cfg_attr(feature = "diesel", diesel(embed))]
30    pub state: UserState,
31}
32
33#[derive(Debug, Clone, Copy)]
34#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow, sqlx::Type))]
35#[cfg_attr(feature = "diesel", derive(diesel::prelude::QueryableByName))]
36pub struct UserState {
37    #[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Bool))]
38    pub is_suspended: bool,
39    #[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Bool))]
40    pub require_email_verification: bool,
41    #[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Bool))]
42    pub require_password_change: bool,
43}
44
45impl <A: App> UserData<A> {
46    /// Indicates whether there is a password associated with this user
47    /// account.
48    pub fn has_password(&self) -> bool {
49        self.password_hash.exists()
50    }
51}
52
53impl UserState {
54    /// A user is "ready" if they are not suspended, and are not required to
55    /// perform some action (email verification or password change) before
56    /// continuing.
57    pub(crate) fn require_ready<ID: Display>(self, user_id: ID) -> Result<(), Error> {
58        if self.is_suspended {
59            log::info!("User #{user_id} is suspended");
60            Err(Error::UserIsSuspended)
61        } else if self.require_email_verification {
62            log::info!("User #{user_id} requires email verification");
63            Err(Error::EmailNotVerified)
64        } else if self.require_password_change {
65            log::info!("User #{user_id} requires password change");
66            Err(Error::RequirePasswordChange)
67        } else {
68            Ok(())
69        }
70    }
71}
72
73/// Registers a new user with a password they have chosen for themselves. An
74/// email verification challenge is sent, and the user must complete this
75/// before they can use their account.
76/// 
77/// Returns the registered user with their unique id.
78pub async fn register_new_user<A: App>(
79    app: &mut A,
80    user: A::User,
81    password: Secret,
82) -> Result<A::User, A::Error> {
83    check_password_strength(app, &password)?;
84    let hash = hashing::generate_password_hash(&password)?;
85    
86    register(app, user, None, hash)
87        .await
88}
89
90/// Registers a new user without a password. An email verification challenge is
91/// sent, and the user must complete this before they can use their account.
92/// After this, the user can log in by completing further email challenges.
93/// 
94/// Returns the registered user with their unique id.
95pub async fn register_new_user_without_password<A: App>(
96    app: &mut A,
97    user: A::User,
98) -> Result<A::User, A::Error> {
99    register(app, user, None, PasswordHash::NONE)
100        .await
101}
102
103/// Registers a new user with a temporary password. Instead of an email
104/// verification challenge, the user is sent an email notification with the
105/// temporary password. When they first login, they will be required to choose
106/// a new password.
107/// 
108/// Returns the registered user with their unique id.
109pub async fn register_new_user_with_temporary_password<A: App>(
110    app: &mut A,
111    user: A::User,
112) -> Result<A::User, A::Error> {
113    let (password, hash) = hashing::generate_password_and_hash()?;
114    
115    register(app, user, Some(password), hash)
116        .await
117}
118
119pub async fn change_password<A: App>(
120    app: &mut A,
121    auth: Auth<A>,
122    old_password: Option<Secret>,
123    new_password: Secret,
124) -> Result<(), A::Error> {
125    let user = auth.user;
126
127    // Make sure they actually changed their password. This doesn't need to be
128    // done in constant-time, because both are provided by the user.
129    if matches!(&old_password, Some(old) if old.0 == new_password.0) {
130        return Error::PasswordsNotDifferent.as_app_err();
131    }
132
133    check_password_strength(app, &new_password)?;
134
135    // Verify the old password.
136    let data = app
137        .get_user_data_by_id(user.id())
138        .await
139        .map_err(Into::into)?
140        .ok_or(Error::UserDataQueryFailed {user_id: user.id().into()})?;
141    
142    // If the account is password-protected, verify the old password
143    if data.password_hash.exists() {
144        if let Some(old_password) = old_password {
145            // Account is password-protected, and old password is provided
146            hashing::verify_password(&data.password_hash, &old_password)?;
147        } else {
148            // Account is password-protected, but no old password is provided
149            return Error::IncorrectPassword.as_app_err();
150        }
151    }
152
153    // Update the password in the database.
154    let new_hash = hashing::generate_password_hash(&new_password)?;
155    app.update_password(&user, new_hash, false)
156        .await
157        .map_err(Into::into)?;
158
159    // Notify the user that their password has been changed, in case they
160    // didn't change it themselves.
161    app.send_notification(&user, Notification::PasswordChanged)
162        .await
163        .map_err(Into::into)?;
164
165    Ok(())
166}
167
168pub async fn request_password_reset<A: App>(app: &mut A, user: &A::User) -> Result<(), A::Error> {
169    issue_challenge(app, user, Challenge::ResetPassword)
170        .await
171}
172
173fn check_password_strength<A: App>(app: &mut A, password: &Secret) -> Result<(), Error> {
174    if password.0.len() < app.minimum_password_length() {
175        return Err(Error::PasswordTooShort);
176    }
177    Ok(())
178}
179
180async fn register<A: App>(
181    app: &mut A,
182    mut user: A::User,
183    temporary_password: Option<Secret>,
184    password_hash: PasswordHash,
185) -> Result<A::User, A::Error> {
186    // Insert the user into the database. This has to be done first to get the
187    // user's new unique id, which might be needed by the app mailer.
188    let user_data = UserData {
189        user: user.clone(),
190        password_hash,
191        state: UserState {
192            is_suspended: false,
193            require_password_change: temporary_password.is_some(),
194            require_email_verification: temporary_password.is_none(),
195        },
196    };
197    let user_id = app.insert_user(user_data)
198        .await
199        .map_err(Into::into)?;
200
201    // Update the user's id.
202    user.set_id(user_id);
203
204    // Send the notification or challenge email.
205    let result = match temporary_password {
206        Some(temporary_password) => {
207            let notification = Notification::UserRegistered {temporary_password};
208            app.send_notification(&user, notification)
209                .await
210                .map_err(Into::into)
211        },
212        None => {
213            issue_challenge(app, &user, Challenge::VerifyNewUser)
214                .await
215                .map_err(Into::into)
216        }
217    };
218
219    if let Err(e) = result {
220        // If sending the email fails, the challenge code or temporary password
221        // are lost, and the user will never be able to log in.
222        app.delete_user(user_id)
223            .await
224            .map_err(Into::into)?;
225
226        return Err(e);
227    }
228
229    Ok(user)
230}