mini_rust_auth/
auth.rs

1//! A set of base functions that are used to do all the underlying work of auth. This includes user and session management.
2use argon2::password_hash::Salt;
3use argon2::Argon2;
4use argon2::PasswordHasher;
5use argon2::PasswordVerifier;
6use dotenvy;
7use rand::distributions::DistString;
8use rand::Rng;
9use sqlx::Postgres;
10use sqlx::QueryBuilder;
11use serde;
12use sqlx::FromRow;
13use serde::ser::SerializeStruct;
14
15pub const SESSION_VALID_FOR_SECONDS: i64 = 3600;
16
17/// Basic information about a user. Note that `realm` can be a arbitrary string and you can use to figure out a group a user belongs to like `"admin"`.
18#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
19pub struct Credentials {
20    pub user_name: String,
21    pub password: String,
22    pub realm: String, // the realm can be used to specify the set of permissions of a user
23}
24
25/// Information about a session. Each session is associated with a `user_name`.
26#[derive(Debug, FromRow)]
27pub struct Session {
28    pub user_name: String,
29    pub session_token: String,
30    pub time_to_die: chrono::DateTime<chrono::Utc>
31}
32
33/// Error return type of `add_user`
34#[derive(Debug)]
35pub enum AddUserReturn {
36    Good(),
37    UserNotUnique(),
38    SaltFailed(),
39    HashError(String),
40    InsertError(String),
41}
42
43#[derive(Debug)]
44pub enum DeleteUserReturn {
45    Good(), // Deleted user
46    FailedToDeleteSessions(String),
47    BadUserOrPassword(),
48    DataBaseError(String),
49}
50
51#[derive(Debug)]
52pub enum EndSessionReturn {
53    Ended(),
54    BadSession(), 
55    DataBaseError(String),
56}
57
58/// Error return type of `validate_user`
59#[derive(Debug)]
60pub enum UserValidatedReturn {
61    Validated(),
62    NotValidated(),
63}
64
65/// Error return type of error in result from `generate_session`
66#[derive(Debug)]
67pub enum SessionGeneratedErr {
68    UserNotValid(),
69    FailedToAddToDatabase(String),
70}
71
72/// Return type of `validate_session`
73#[derive(Debug)]
74pub enum SessionValidated {
75    ValidSession(),
76    InvalidSession(), 
77}
78
79/// Return type of `invalidate_session`
80pub enum SessionInvalided {
81    SucessfullyInvalidated(),
82    DidNotExist(),
83    Error(String),
84}
85
86#[derive(FromRow)]
87struct UserRow {
88    user_name: String,
89    phc: String,
90    realms: String,
91}
92
93impl serde::Serialize for Session{
94    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
95    where
96        S: serde::Serializer,
97    {
98        let mut s = serializer.serialize_struct("Session", 3)?;
99        s.serialize_field("user_name", &self.user_name)?;
100        s.serialize_field("session_token", &self.session_token)?;
101        s.serialize_field("time_to_die", &self.time_to_die.to_rfc3339())?;
102        s.end()
103    }
104}
105
106/// Generates a random string between the given ranges.
107fn gen_rand_string_between(min_len: u16, max_len: u16) -> String {
108    let mut rng_source = rand::thread_rng();
109    let str_len: usize = rng_source.gen_range((min_len + 1)..max_len).into();
110
111    rand::distributions::Alphanumeric.sample_string(&mut rng_source, str_len)
112}
113
114/// Generates a random string of a given length.
115fn gen_rand_string_of_len(len: u16) -> String {
116    let mut rng_source = rand::thread_rng();
117    rand::distributions::Alphanumeric.sample_string(&mut rng_source, len.into())
118}
119
120/// Provides a connection to a postgres server. This requires `DATABASE_URL` to be set in `.env` file.
121pub async fn connect_to_db_get_pool() -> Result<sqlx::Pool<Postgres>, sqlx::Error> {
122    let dotenv_database_url_result = dotenvy::var("DATABASE_URL");
123    let data_baseurl = match dotenv_database_url_result {
124        Ok(data_baseurl) => data_baseurl,
125        Err(err) => err.to_string(),
126    };
127
128    // Connect to database
129    let pool = match sqlx::postgres::PgPoolOptions::new()
130        .max_connections(5)
131        .connect(&data_baseurl)
132        .await
133    {
134        Ok(pool) => pool,
135        Err(err) => return Err(err),
136    };
137
138    return Ok(pool);
139}
140
141/// Builds the projects current default Argon2 harsher. This exists so that
142/// we avoid `default` and grantee the same settings for the harsher across the
143/// As some point in the future it would be good if this was loaded
144/// from ENV.
145fn build_argon2_hasher<'a>() -> Argon2<'a> {
146    argon2::Argon2::new(
147        argon2::Algorithm::Argon2id,
148        argon2::Version::V0x13,
149        argon2::Params::new(1500, 2, 1, None).expect("Argon2 failed to generate harsher"),
150    )
151}
152
153/// Will create a session for the provided user. The users information is stored
154/// in `credentials`. Once the session is generated it will be stored in the
155/// DB. The validity and state of the session can be changed later with
156/// `validate_session` and `invalidate_session`.
157pub async fn generate_session(
158    credentials: &Credentials,
159    pool: &sqlx::Pool<Postgres>,
160    secs_after_creation_to_die: i64,
161) -> Result<Session, SessionGeneratedErr> {
162    // Check if user is valid in DB we will not use this info later so we toss it
163    let _valid_user = match validate_user(&credentials, pool).await {
164        UserValidatedReturn::Validated() => (),
165        UserValidatedReturn::NotValidated() => return Err(SessionGeneratedErr::UserNotValid()), 
166    };
167    
168    let session = Session{
169        user_name: credentials.user_name.to_string(),
170        session_token: gen_rand_string_of_len(100),
171        time_to_die: chrono::Utc::now() + chrono::TimeDelta::seconds(secs_after_creation_to_die)
172    };
173
174    let mut sql_insert_session_builder = sqlx::QueryBuilder::new("INSERT INTO sessions (user_name, session_token, time_to_die) VALUES (");
175    sql_insert_session_builder.push_bind(&session.user_name);
176    sql_insert_session_builder.push(",");
177    sql_insert_session_builder.push_bind(&session.session_token);
178    sql_insert_session_builder.push(",");
179    sql_insert_session_builder.push_bind(session.time_to_die.to_rfc3339());
180    sql_insert_session_builder.push("::timestamp);");
181    
182    let sql_insert_session = sql_insert_session_builder.build().execute(pool).await;
183
184    match sql_insert_session {
185        Ok(query_result) => match  query_result.rows_affected() {
186            1 => Ok(session),
187            _ => {
188                println!("Modified: {}", query_result.rows_affected());
189                Err(SessionGeneratedErr::FailedToAddToDatabase("Got more than 0 changes".to_string()))
190            }
191        },
192        Err(err) => Err(SessionGeneratedErr::FailedToAddToDatabase(format!("{}", err.to_string())))
193    }
194}
195
196/// `validate_session`
197///
198/// Given a session will check if its valid in the database.
199pub async fn validate_session(
200    session: &Session,
201    pool: &sqlx::Pool<Postgres>,
202) -> SessionValidated {
203    let sql_session = "SELECT user_name, session_token FROM sessions WHERE user_name=$1 AND session_token=$2 AND time_to_die > now() at time zone ('utc');";
204
205    let (db_user_name, db_session_token):(String, String) = match sqlx::query_as(&sql_session)
206    .bind(&session.user_name)
207    .bind(&session.session_token)
208    .fetch_optional(pool).await {
209        Ok(option) => match  option {
210            Some(res) => res,
211            None => return SessionValidated::InvalidSession()
212        },
213        Err(_err) => return SessionValidated::InvalidSession()
214    };
215
216    // This is double checking the work of the db so may not really be needed
217    // However better safe than sorry
218    if db_user_name == session.user_name && db_session_token == session.session_token {
219        return SessionValidated::ValidSession();
220    } else {
221        return SessionValidated::InvalidSession();
222    }
223}
224
225/// `invalidate_session`
226/// 
227/// If the session exists in the database will drop the session. This means the user will no longer be able use this session from any request. 
228pub async fn invalidate_session(
229    session: &Session,
230    pool: &sqlx::Pool<Postgres>,
231) -> SessionInvalided {
232    let sql_session = "DELETE FROM sessions WHERE user_name=$1 AND session_token=$2;";
233
234    match sqlx::query(&sql_session)
235    .bind(&session.user_name)
236    .bind(&session.session_token)
237    .execute(pool).await {
238        Ok(res) => match res.rows_affected() {
239           1 => SessionInvalided::SucessfullyInvalidated(),
240           _ => SessionInvalided::DidNotExist(),
241        },
242        Err(err) => return SessionInvalided::Error(err.to_string())
243    }
244}
245
246
247/// `add_user`
248/// 
249/// Given the user information. Will add the user to the database. 
250/// This will allow the user to generate sessions with the given user information.
251pub async fn add_user(credentials: &Credentials, pool: &sqlx::Pool<Postgres>) -> AddUserReturn {
252    // Generate Hasher instancef
253    // Generate salt
254    let rand_str = gen_rand_string_of_len(Salt::RECOMMENDED_LENGTH.try_into().unwrap());
255    let hasher = build_argon2_hasher();
256
257    // Encode string to phc
258    let hash = hasher.hash_password(credentials.password.as_bytes(), &rand_str);
259    let phc_string = match hash {
260        Ok(phc_string) => phc_string.to_string(),
261        Err(err) => {
262            return AddUserReturn::HashError(format!(
263                "with pass {}, salt {}, got {}",
264                credentials.password,
265                4,
266                err.to_string()
267            ))
268        }
269    };
270    println!("with pass {}, salt {}", credentials.password, rand_str);
271
272    // Build SQL
273    let mut sql_insert_user_builder: QueryBuilder<Postgres> =
274        sqlx::QueryBuilder::new("INSERT INTO users(user_name, phc, realms) VALUES (");
275    sql_insert_user_builder
276        .push_bind(&credentials.user_name)
277        .push(",")
278        .push_bind(phc_string)
279        .push(",")
280        .push_bind(&credentials.realm)
281        .push(") LIMIT 1;");
282
283    let sql_insert_build: sqlx::query::QueryAs<Postgres, UserRow, sqlx::postgres::PgArguments> =
284        sql_insert_user_builder.build_query_as::<UserRow>();
285    match sql_insert_build.fetch_optional(pool).await {
286        Ok(_) => return AddUserReturn::Good(),
287        Err(err) => {
288            let _db_err = match err.as_database_error() {
289                Some(db_err) => 
290                match db_err.to_string().find("users_pkey") {
291                    Some(_match) => return AddUserReturn::UserNotUnique(),
292                    None => return AddUserReturn::InsertError(err.to_string())
293                }
294                None => return AddUserReturn::InsertError(err.to_string())
295            };
296        },
297    }
298}
299
300/// `delete_user`
301/// 
302/// Remove a user given proper credentials
303pub async fn delete_user(credentials: &Credentials, pool: &sqlx::Pool<Postgres>) -> DeleteUserReturn {
304    // Check if user is valid in DB we will not use this info later so we toss it
305    let _valid_user = match validate_user(&credentials, pool).await {
306        UserValidatedReturn::Validated() => (),
307        UserValidatedReturn::NotValidated() => return DeleteUserReturn::BadUserOrPassword()
308    };
309    // Delete Sessions first
310    let mut sql_delete_session_builder = sqlx::QueryBuilder::new("DELETE FROM sessions WHERE user_name=");
311
312    sql_delete_session_builder.push_bind(&credentials.user_name).push(";");
313
314    let _sessions_deleted_option = match sql_delete_session_builder.build().fetch_optional(pool).await {
315        Ok(_) => (),
316        Err(err) => return DeleteUserReturn::FailedToDeleteSessions(err.to_string())
317    };
318
319    // Delete user
320    let mut sql_delete_use_builder = sqlx::QueryBuilder::new("DELETE FROM users WHERE user_name =");
321
322    sql_delete_use_builder.push_bind(&credentials.user_name).push(";");
323
324    let _user_deleted_option = match sql_delete_use_builder.build().fetch_optional(pool).await {
325        Ok(_) => return DeleteUserReturn::Good(),
326        Err(err) => return  DeleteUserReturn::DataBaseError(err.to_string())
327    };
328}
329
330/// `end_sessions`
331/// 
332/// Will drop all sessions for the users session table
333pub async fn end_sessions(session: &Session, pool: &sqlx::Pool<Postgres>) -> EndSessionReturn {
334    let _valid_session = match validate_session(session, pool).await {
335        SessionValidated::ValidSession() => (),
336        SessionValidated::InvalidSession() => return EndSessionReturn::BadSession()
337    };
338
339    let mut sql_delete_session_builder = sqlx::QueryBuilder::new("DELETE FROM sessions WHERE user_name=");
340
341    sql_delete_session_builder.push_bind(&session.user_name).push(";");
342
343    let _sessions_deleted_option = match sql_delete_session_builder.build().fetch_optional(pool).await {
344        Ok(_) => return  EndSessionReturn::Ended(),
345        Err(err) => return EndSessionReturn::DataBaseError(err.to_string())
346    };
347}
348
349/// `validate_user`
350/// 
351/// Given the user credentials provided will check if the credentials are valid and in the database.
352pub async fn validate_user(credentials: &Credentials, pool: &sqlx::Pool<Postgres>) -> UserValidatedReturn {
353    //! 1. Get user from data base
354    //! Build SQL for SELECT
355    let mut sql_user_builder: QueryBuilder<Postgres> =
356        sqlx::QueryBuilder::new("SELECT user_name, phc, realms FROM users WHERE user_name=");
357
358    sql_user_builder
359        .push_bind(credentials.user_name.clone())
360        .push(";");
361
362    let user_info_option: Option<UserRow> = match sql_user_builder
363        .build_query_as::<UserRow>()
364        .fetch_optional(pool)
365        .await
366    {
367        Ok(user_info_option) => user_info_option,
368        Err(_) => {
369            println!("Fail at fetch");
370            return UserValidatedReturn::NotValidated();
371        }
372    };
373
374    let user_info: UserRow = match user_info_option {
375        Some(user_info) => user_info,
376        None => {
377            println!("Fail at User info up");
378            return UserValidatedReturn::NotValidated();
379        }
380    };
381
382    // once we have the user info and have confirmed it exists we can check the
383    let existing_password_hash: argon2::PasswordHash =
384        match argon2::PasswordHash::new(&user_info.phc) {
385            Ok(hasher) => hasher,
386            Err(_) => {
387                println!("Fail at hash");
388                return UserValidatedReturn::NotValidated();
389            }
390        };
391
392    let argon_hasher = build_argon2_hasher(); // Builds Argon hasher is current default settings
393    match argon_hasher.verify_password(credentials.password.as_bytes(), &existing_password_hash) {
394        Ok(_) => UserValidatedReturn::Validated(),
395        Err(_) => UserValidatedReturn::NotValidated(),
396    }
397}