diff --git a/.gitignore b/.gitignore index c977c1a..b272805 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ Cargo.lock examples/database.db .vscode -.DS_Store \ No newline at end of file +.DS_Store +database.db \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 32749e5..72d5828 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_auth" -version = "0.4.0" +version = "0.5.0" authors = ["tvallotton@uc.cl"] edition = "2018" license = "MIT or Apache-2.0" @@ -29,31 +29,31 @@ optional = true [dependencies] -rand = "0.8.5" -rust-argon2 = "1.0.0" -lazy_static = "1.4.0" -regex = "1.5.6" -serde_json = "1.0.82" -chashmap = "2.2.2" -thiserror = "1.0.31" -async-trait = "0.1.56" -fehler = "1.0.0" -chrono = "0.4.19" -validator = { version = "0.15.0", features = ["derive"] } -futures= "0.3.21" +rand = ">=0.8.5" +rust-argon2 = ">=1.0.0" +lazy_static = ">=1.4.0" +regex = ">=1.5.6" +serde_json = ">=1.0.82" +chashmap = ">=2.2.2" +thiserror = ">=1.0.31" +async-trait = ">=0.1.56" +fehler = ">=1.0.0" +chrono = ">=0.4.19" +validator = { version = ">=0.15.0", features = ["derive"] } +futures= ">=0.3.21" [dependencies.sqlx] -version = "0.6.0" +version = ">=0.6.0" optional = true [dependencies.rocket] -version = "0.5.0-rc.2" +version = ">=0.5.0" features = ["secrets"] [dependencies.serde] -version = "1.0.138" +version = ">=1.0.138" features = ["derive"] [dependencies.tokio-postgres] @@ -75,7 +75,7 @@ tokio-postgres= "0.7.6" [dev-dependencies.rocket] -version = "0.5.0-rc.2" +version = ">=0.5.0" features = ["secrets", "json"] [dev-dependencies.redis] @@ -84,12 +84,12 @@ features = ["aio", "tokio-comp"] [dev-dependencies.rocket_dyn_templates] -version = "0.1.0-rc.2" +version = ">=0.1.0" features = ["tera"] [dev-dependencies.sqlx] -version = "0.6.0" +version = ">=0.6.0" features = ["runtime-tokio-rustls"] [dev-dependencies.rocket_auth] diff --git a/database.db b/database.db new file mode 100644 index 0000000..1277baf Binary files /dev/null and b/database.db differ diff --git a/src/cookies.rs b/src/cookies.rs index aed56c9..8dcb318 100644 --- a/src/cookies.rs +++ b/src/cookies.rs @@ -2,6 +2,7 @@ use crate::prelude::*; use rocket::http::{CookieJar, Status}; use rocket::request::{FromRequest, Outcome, Request}; use serde_json::from_str; +use crate::error; /// The Session guard can be used to retrieve user session data. /// Unlike `User`, using session does not verify that the session data is @@ -34,7 +35,7 @@ impl<'r> FromRequest<'r> for Session { if let Some(session) = get_session(cookies) { Outcome::Success(session) } else { - Outcome::Failure((Status::Unauthorized, Error::UnauthorizedError)) + Outcome::Error((Status::Unauthorized, error::Error::UnauthorizedError)) } } } diff --git a/src/forms/mod.rs b/src/forms/mod.rs index 1158ba6..d6f9ed9 100644 --- a/src/forms/mod.rs +++ b/src/forms/mod.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use validator::Validate; /// The `Login` form is used along with the [`Auth`] guard to authenticate users. @@ -14,12 +15,7 @@ pub struct Login { pub struct Signup { #[validate(email)] pub email: String, - #[validate( - custom = "is_long", - custom = "has_number", - custom = "has_lowercase", - custom = "has_uppercase" - )] + #[validate(length(min=8),custom(function=is_secure))] pub(crate) password: String, } impl Debug for Signup { diff --git a/src/lib.rs b/src/lib.rs index 33092bb..d97f778 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ //! //! //! To use `rocket_auth` include it as a dependency in your Cargo.toml file: -//! ``` +//! ```toml //! [dependencies.rocket_auth] //! version = "0.4.0" //! features = ["sqlx-sqlite"] diff --git a/src/user/auth.rs b/src/user/auth.rs index d626cbf..5da1ded 100644 --- a/src/user/auth.rs +++ b/src/user/auth.rs @@ -7,6 +7,21 @@ use rocket::Request; use rocket::State; use serde_json::json; use std::time::Duration; +use regex::Regex; + + + + /// Validates an email address (helper function). +pub fn validate_email(email: &String) -> bool { + let expr = Regex::new("^[\\w\\.-]+@[\\w\\.-]+\\.[a-zA-Z]{2,6}$"); + + if let Ok(reg_ex) = expr { + return reg_ex.is_match(&email) + } else { + return false + } + +} /// The [`Auth`] guard allows to log in, log out, sign up, modify, and delete the currently (un)authenticated user. /// For more information see [`Auth`]. @@ -64,7 +79,7 @@ impl<'r> FromRequest<'r> for Auth<'r> { let users: &State = if let Outcome::Success(users) = req.guard().await { users } else { - return Outcome::Failure((Status::InternalServerError, Error::UnmanagedStateError)); + return Outcome::Error((Status::InternalServerError, Error::UnmanagedStateError)); }; Outcome::Success(Auth { @@ -222,7 +237,7 @@ impl<'a> Auth<'a> { pub fn logout(&self) { let session = self.get_session()?; self.users.logout(session)?; - self.cookies.remove_private(Cookie::named("rocket_auth")); + self.cookies.remove_private(Cookie::build("rocket_auth")); } /// Deletes the account of the currently authenticated user. /// ```rust @@ -238,7 +253,7 @@ impl<'a> Auth<'a> { if self.is_auth() { let session = self.get_session()?; self.users.delete(session.id).await?; - self.cookies.remove_private(Cookie::named("rocket_auth")); + self.cookies.remove_private("rocket_auth"); } else { throw!(Error::UnauthenticatedError) } @@ -253,13 +268,14 @@ impl<'a> Auth<'a> { /// auth.change_password("new password"); /// # } /// ``` - #[throws(Error)] - pub async fn change_password(&self, password: &str) { + pub async fn change_password(&self, password: &str) -> Result<(), Box> { if self.is_auth() { let session = self.get_session()?; let mut user = self.users.get_by_id(session.id).await?; user.set_password(password)?; self.users.modify(&user).await?; + + Ok(()) } else { throw!(Error::UnauthorizedError) } @@ -272,18 +288,18 @@ impl<'a> Auth<'a> { /// auth.change_email("new@email.com".into()); /// # } /// ``` - #[throws(Error)] - pub async fn change_email(&self, email: String) { + pub async fn change_email(&self, email: String) -> Result<(), Error> { if self.is_auth() { - if !validator::validate_email(&email) { - throw!(Error::InvalidEmailAddressError) + if !validate_email(&email) { + return Err(Error::InvalidEmailAddressError) } let session = self.get_session()?; let mut user = self.users.get_by_id(session.id).await?; user.email = email.to_lowercase(); self.users.modify(&user).await?; + return Ok(()) } else { - throw!(Error::UnauthorizedError) + return Err(Error::UnauthorizedError) } } @@ -316,3 +332,21 @@ impl<'a> Auth<'a> { } } } + + + +#[cfg(test)] +mod test { + + use super::validate_email; + + + #[test] + fn test_validate_email() { + + let good_mail = String::from("some.example@gmail.com"); + let bad_mail = String::from("@fak,.r"); + assert!(validate_email(&good_mail)); + assert!(!(validate_email(&bad_mail))); + } +} \ No newline at end of file diff --git a/src/user/user_impl.rs b/src/user/user_impl.rs index e5ebe05..0db861e 100644 --- a/src/user/user_impl.rs +++ b/src/user/user_impl.rs @@ -1,9 +1,10 @@ -use super::auth::Auth; +use super::auth::{Auth, validate_email}; use super::rand_string; use crate::prelude::*; use rocket::http::Status; use rocket::request::{FromRequest, Outcome, Request}; +use crate::error; impl User { /// This method allows to reset the password of a user. @@ -14,8 +15,8 @@ impl User { /// This function will fail in case the password is not secure enough. /// /// ```rust - /// # use rocket::{State, post}; - /// # use rocket_auth::{Error, Users}; + /// use rocket::{State, post}; + /// use rocket_auth::{Error, Users, User}; /// #[post("/reset-password/")] /// async fn reset_password(mut user: User, users: &State, new_password: String) -> Result<(), Error> { /// user.set_password(&new_password); @@ -23,30 +24,30 @@ impl User { /// Ok(()) /// } /// ``` - #[throws(Error)] - pub fn set_password(&mut self, new: &str) { + pub fn set_password(&mut self, new: &str) -> Result<(), Box> { crate::forms::is_secure(new)?; let password = new.as_bytes(); let salt = rand_string(10); let config = argon2::Config::default(); - let hash = argon2::hash_encoded(password, salt.as_bytes(), &config).unwrap(); + let hash = argon2::hash_encoded(password, salt.as_bytes(), &config)?; self.password = hash; + Ok(()) } /// Compares the password of the currently authenticated user with a another password. /// Useful for checking password before resetting email/password. /// To avoid bruteforcing this function should not be directly accessible from a route. /// Additionally, it is good to implement rate limiting on routes using this function. - #[throws(Error)] - pub fn compare_password(&self, password: &str) -> bool { - verify_encoded(&self.password, password.as_bytes())? + + pub fn compare_password(&self, password: &str) -> Result { + verify_encoded(&self.password, password.as_bytes()) } /// This is an accessor function for the private `id` field. /// This field is private, so that it is not modified by accident when updating a user. /// ```rust - /// # use rocket::{State, get}; - /// # use rocket_auth::{Error, User}; + /// use rocket::{State, get}; + /// use rocket_auth::{Error, User}; /// #[get("/show-my-id")] /// fn show_my_id(user: User) -> String { /// format!("Your user_id is: {}", user.id()) @@ -58,8 +59,8 @@ impl User { /// This is an accessor field for the private `email` field. /// This field is private so an email cannot be updated without checking whether it is valid. /// ```rust - /// # use rocket::{State, get}; - /// # use rocket_auth::{Error, User}; + /// use rocket::{State, get}; + /// use rocket_auth::{Error, User}; /// #[get("/show-my-email")] /// fn show_my_email(user: User) -> String { /// format!("Your user_id is: {}", user.email()) @@ -78,15 +79,15 @@ impl User { /// #[get("/set-email/")] /// async fn set_email(email: String, auth: Auth<'_>) -> Result { /// let mut user = auth.get_user().await.unwrap(); - /// user.set_email(&email)?; + /// user.set_email(email)?; /// auth.users.modify(&user).await?; /// Ok("Your user email was changed".into()) /// } /// ``` - #[throws(Error)] - pub fn set_email(&mut self, email: &str) { - if validator::validate_email(email) { + pub fn set_email(&mut self, email: String) -> Result<(), Error>{ + if validate_email(&email) { self.email = email.to_lowercase(); + Ok(()) } else { throw!(Error::InvalidEmailAddressError) } @@ -113,13 +114,13 @@ impl<'r> FromRequest<'r> for User { let guard = request.guard().await; let auth: Auth = match guard { Success(auth) => auth, - Failure(x) => return Failure(x), + Error(x) => return Error(x), Forward(x) => return Forward(x), }; if let Some(user) = auth.get_user().await { Outcome::Success(user) } else { - Outcome::Failure((Status::Unauthorized, Error::UnauthorizedError)) + Outcome::Error((Status::Unauthorized, error::Error::UserNotFoundError)) } } } @@ -132,7 +133,7 @@ impl<'r> FromRequest<'r> for AdminUser { let guard = request.guard().await; let auth: Auth = match guard { Success(auth) => auth, - Failure(x) => return Failure(x), + Error(x) => return Error(x), Forward(x) => return Forward(x), }; if let Some(user) = auth.get_user().await { @@ -140,11 +141,11 @@ impl<'r> FromRequest<'r> for AdminUser { return Outcome::Success(AdminUser(user)); } } - Outcome::Failure((Status::Unauthorized, Error::UnauthorizedError)) + Outcome::Error((Status::Unauthorized, error::Error::UnauthorizedError)) } } -use std::ops::*; +use std::{ops::*, result}; use argon2::verify_encoded; impl Deref for AdminUser { diff --git a/src/user/users.rs b/src/user/users.rs index e98a174..e277dbd 100644 --- a/src/user/users.rs +++ b/src/user/users.rs @@ -35,20 +35,19 @@ impl Users { /// It is necessary to call it explicitly when casting the `Users` struct from an already /// established database connection and if the table hasn't been created yet. If the table /// already exists then this step is not necessary. - /// ```rust, - /// # use sqlx::{sqlite::SqlitePool, Connection}; - /// # use rocket_auth::{Users, Error}; + /// ```rust + /// use sqlx::{sqlite::SqlitePool, Connection}; + /// use rocket_auth::{Users, Error}; /// # #[tokio::main] /// # async fn main() -> Result<(), Error> { - /// let mut conn = SqlitePool::connect("database.db").await?; + /// let mut conn = SqlitePool::connect("./database.db").await?; /// let mut users: Users = conn.into(); /// users.open_redis("redis://127.0.0.1/")?; /// users.create_table().await?; /// # Ok(()) } /// ``` - #[throws(Error)] - pub async fn create_table(&self) { - self.conn.init().await? + pub async fn create_table(&self) -> Result<(), Error> { + self.conn.init().await } /// Opens a redis connection. It allows for sessions to be stored persistently across /// different launches. Note that persistent sessions also require a `secret_key` to be set in the [Rocket.toml](https://rocket.rs/v0.5-rc/guide/configuration/#configuration) configuration file. @@ -66,10 +65,10 @@ impl Users { /// # Ok(()) } /// ``` #[cfg(feature = "redis")] - #[throws(Error)] - pub fn open_redis(&mut self, path: impl redis::IntoConnectionInfo) { + pub fn open_redis(&mut self, path: impl redis::IntoConnectionInfo) -> Result<(), Error> { let client = redis::Client::open(path)?; self.sess = Box::new(client); + Ok(()) } /// It creates a `Users` instance by connecting it to a sqlite database. @@ -90,15 +89,14 @@ impl Users { /// # Ok(()) } /// ``` #[cfg(feature = "rusqlite")] - #[throws(Error)] - pub fn open_rusqlite(path: impl AsRef) -> Self { + pub fn open_rusqlite(path: impl AsRef) -> Result { use tokio::sync::Mutex; let users = Users { conn: Box::new(Mutex::new(rusqlite::Connection::open(path)?)), sess: Box::new(chashmap::CHashMap::new()), }; futures::executor::block_on(users.conn.init())?; - users + Ok(users) } /// It creates a `Users` instance by connecting it to a postgres database. @@ -117,8 +115,7 @@ impl Users { /// /// ``` #[cfg(feature = "sqlx-postgres")] - #[throws(Error)] - pub async fn open_postgres(path: &str) -> Self { + pub async fn open_postgres(path: &str) -> Result { use sqlx::PgPool; let conn = PgPool::connect(path).await?; conn.init().await?; @@ -126,7 +123,7 @@ impl Users { conn: Box::new(conn), sess: Box::new(chashmap::CHashMap::new()), }; - users + Ok(users) } /// It creates a `Users` instance by connecting it to a mysql database. @@ -145,12 +142,11 @@ impl Users { /// ``` #[cfg(feature = "sqlx-mysql")] - #[throws(Error)] - pub async fn open_mysql(path: &str) -> Self { + pub async fn open_mysql(path: &str) -> Result { let conn = sqlx::MySqlPool::connect(path).await?; let users: Users = conn.into(); users.create_table().await?; - users + Ok(users) } /// It queries a user by their email. @@ -165,9 +161,8 @@ impl Users { /// } /// # fn main() {} /// ``` - #[throws(Error)] - pub async fn get_by_email(&self, email: &str) -> User { - self.conn.get_user_by_email(email).await? + pub async fn get_by_email(&self, email: &str) -> Result { + self.conn.get_user_by_email(email).await } /// It queries a user by their email. @@ -182,9 +177,9 @@ impl Users { /// # } /// # fn main() {} /// ``` - #[throws(Error)] - pub async fn get_by_id(&self, user_id: i32) -> User { - self.conn.get_user_by_id(user_id).await? + pub async fn get_by_id(&self, user_id: i32) -> Result { + self.conn.get_user_by_id(user_id).await + } /// Inserts a new user in the database. It will fail if the user already exists. @@ -198,22 +193,25 @@ impl Users { /// } /// # fn main() {} /// ``` - #[throws(Error)] - pub async fn create_user(&self, email: &str, password: &str, is_admin: bool) { + pub async fn create_user(&self, email: &str, password: &str, is_admin: bool) -> Result<(), Error> { let password = password.as_bytes(); let salt = rand_string(30); let config = argon2::Config::default(); let hash = argon2::hash_encoded(password, salt.as_bytes(), &config).unwrap(); self.conn.create_user(email, &hash, is_admin).await?; + + Ok(()) } /// Deletes a user from de database. Note that this method won't delete the session. /// To do that use [`Auth::delete`](crate::Auth::delete). - /// ``` + /// ```rust + /// use rocket::{get, State}; + /// use rocket_auth::{Users, User, Error}; /// #[get("/delete_user/")] - /// async fn delete_user(id: i32, users: &State) -> Result { + /// async fn delete_user(id: i32, users: &State) -> Result { /// users.delete(id).await?; - /// Ok("The user has been deleted.") + /// Ok(String::from("The user has been deleted.")) /// } /// ``` #[throws(Error)] @@ -227,7 +225,7 @@ impl Users { /// # use rocket_auth::{Users, Error}; /// # async fn func(users: Users) -> Result<(), Error> { /// let mut user = users.get_by_id(4).await?; - /// user.set_email("new@email.com"); + /// user.set_email("new@email.com".to_string()); /// user.set_password("new password"); /// users.modify(&user).await?; /// # Ok(())} @@ -240,8 +238,8 @@ impl Users { /// A `Users` instance can also be created from a database connection. /// ```rust -/// # use rocket_auth::{Users, Error}; -/// # use tokio_postgres::NoTls; +/// use rocket_auth::{Users, Error}; +/// use tokio_postgres::NoTls; /// # async fn func() -> Result<(), Error> { /// let (client, connection) = tokio_postgres::connect("host=localhost user=postgres", NoTls).await?; /// let users: Users = client.into();