diff --git a/src/auth.rs b/src/auth/jwt.rs similarity index 57% rename from src/auth.rs rename to src/auth/jwt.rs index efd991d..8767bb3 100644 --- a/src/auth.rs +++ b/src/auth/jwt.rs @@ -1,4 +1,3 @@ -use argon2::verify_encoded; use chrono::Utc; use diesel::{insert_into, prelude::*, PgConnection}; use hmac::{Hmac, NewMac}; @@ -10,31 +9,12 @@ use sha2::Sha256; use crate::{ db::{ tokens::{NewRefreshToken, RefreshToken}, - users::{NewUser, User}, + users::User, }, - errors::RBError, + errors::RbError, schema::{refresh_tokens::dsl as refresh_tokens, users::dsl as users}, }; -pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result -{ - // TODO handle non-"NotFound" Diesel errors accordingely - let user = users::users - .filter(users::username.eq(username)) - .first::(conn) - .map_err(|_| RBError::UnknownUser)?; - - // Check if a user is blocked - if user.blocked { - return Err(RBError::BlockedUser); - } - - match verify_encoded(user.password.as_str(), password.as_bytes()) { - Ok(true) => Ok(user), - _ => Err(RBError::InvalidPassword), - } -} - #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct JWTResponse @@ -54,9 +34,9 @@ pub struct Claims pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result { - let secret = std::env::var("JWT_KEY").map_err(|_| RBError::MissingJWTKey)?; - let key: Hmac = - Hmac::new_from_slice(secret.as_bytes()).map_err(|_| RBError::JWTCreationError)?; + let secret = std::env::var("JWT_KEY").map_err(|_| RbError::Custom("Missing JWT key."))?; + let key: Hmac = Hmac::new_from_slice(secret.as_bytes()) + .map_err(|_| RbError::Custom("Couldn't create Hmac key."))?; let current_time = Utc::now(); @@ -71,7 +51,7 @@ pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result crate::Result crate::Result crate::Result -{ - // Generate a random salt - let mut salt = [0u8; 64]; - thread_rng().fill(&mut salt[..]); - - // Encode the actual password - let config = argon2::Config::default(); - argon2::hash_encoded(password.as_bytes(), &salt, &config).map_err(|_| RBError::PWSaltError) -} - -pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) - -> crate::Result -{ - let pass_hashed = hash_password(password)?; - let new_user = NewUser { - username: username.to_string(), - password: pass_hashed, - admin: true, - }; - - insert_into(users::users) - .values(&new_user) - .on_conflict(users::username) - .do_update() - .set(&new_user) - .execute(conn) - .map_err(|_| RBError::AdminCreationError)?; - - Ok(true) -} - pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> crate::Result { - let token_bytes = base64::decode(refresh_token).map_err(|_| RBError::InvalidRefreshToken)?; + let token_bytes = + base64::decode(refresh_token).map_err(|_| RbError::AuthInvalidRefreshToken)?; // First, we request the token from the database to see if it's really a valid token let (token_entry, user) = refresh_tokens::refresh_tokens .inner_join(users::users) .filter(refresh_tokens::token.eq(token_bytes)) .first::<(RefreshToken, User)>(conn) - .map_err(|_| RBError::InvalidRefreshToken)?; + .map_err(|_| RbError::AuthInvalidRefreshToken)?; // If we see that the token has already been used before, we block the user. if token_entry.last_used_at.is_some() { @@ -145,16 +94,16 @@ pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> crate::Result< diesel::update(target) .set(users::blocked.eq(true)) .execute(conn) - .map_err(|_| RBError::DBError)?; + .map_err(|_| RbError::Custom("Couldn't block user."))?; - return Err(RBError::DuplicateRefreshToken); + return Err(RbError::AuthDuplicateRefreshToken); } // Now we check if the token has already expired let cur_time = Utc::now().naive_utc(); if token_entry.expires_at < cur_time { - return Err(RBError::TokenExpired); + return Err(RbError::AuthTokenExpired); } // We update the last_used_at value for the refresh token @@ -162,7 +111,7 @@ pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> crate::Result< diesel::update(target) .set(refresh_tokens::last_used_at.eq(cur_time)) .execute(conn) - .map_err(|_| RBError::DBError)?; + .map_err(|_| RbError::Custom("Couldn't update last used time."))?; generate_jwt_token(conn, &user) } diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..b315e82 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,63 @@ +use argon2::verify_encoded; +use diesel::{insert_into, prelude::*, PgConnection}; +use rand::{thread_rng, Rng}; + +use crate::{ + db::users::{NewUser, User}, + errors::RbError, + schema::users::dsl as users, +}; + +pub mod jwt; + +pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result +{ + // TODO handle non-"NotFound" Diesel errors accordingely + let user = users::users + .filter(users::username.eq(username)) + .first::(conn) + .map_err(|_| RbError::AuthUnknownUser)?; + + // Check if a user is blocked + if user.blocked { + return Err(RbError::AuthBlockedUser); + } + + match verify_encoded(user.password.as_str(), password.as_bytes()) { + Ok(true) => Ok(user), + _ => Err(RbError::AuthInvalidPassword), + } +} + +pub fn hash_password(password: &str) -> crate::Result +{ + // Generate a random salt + let mut salt = [0u8; 64]; + thread_rng().fill(&mut salt[..]); + + // Encode the actual password + let config = argon2::Config::default(); + argon2::hash_encoded(password.as_bytes(), &salt, &config) + .map_err(|_| RbError::Custom("Couldn't hash password.")) +} + +pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) + -> crate::Result +{ + let pass_hashed = hash_password(password)?; + let new_user = NewUser { + username: username.to_string(), + password: pass_hashed, + admin: true, + }; + + insert_into(users::users) + .values(&new_user) + .on_conflict(users::username) + .do_update() + .set(&new_user) + .execute(conn) + .map_err(|_| RbError::Custom("Couldn't create admin."))?; + + Ok(true) +} diff --git a/src/db/mod.rs b/src/db/mod.rs index bf3b714..9c831dd 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,2 +1,5 @@ pub mod tokens; pub mod users; + +pub use tokens::{NewRefreshToken, RefreshToken}; +pub use users::{NewUser, User}; diff --git a/src/db/users.rs b/src/db/users.rs index b005295..79d337a 100644 --- a/src/db/users.rs +++ b/src/db/users.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - errors::RBError, + errors::RbError, schema::{users, users::dsl::*}, }; @@ -30,7 +30,9 @@ pub struct NewUser pub fn all(conn: &PgConnection) -> crate::Result> { - users.load::(conn).map_err(|_| RBError::DBError) + users + .load::(conn) + .map_err(|_| RbError::DbError("Couldn't get all users.")) } pub fn find(conn: &PgConnection, user_id: Uuid) -> Option @@ -43,10 +45,10 @@ pub fn create(conn: &PgConnection, new_user: &NewUser) -> crate::Result<()> let count = diesel::insert_into(users) .values(new_user) .execute(conn) - .map_err(|_| RBError::DBError)?; + .map_err(|_| RbError::DbError("Couldn't create user."))?; if count == 0 { - return Err(RBError::DuplicateUser); + return Err(RbError::UMDuplicateUser); } Ok(()) @@ -56,7 +58,7 @@ pub fn delete(conn: &PgConnection, user_id: Uuid) -> crate::Result<()> { diesel::delete(users.filter(id.eq(user_id))) .execute(conn) - .map_err(|_| RBError::DBError)?; + .map_err(|_| RbError::DbError("Couldn't delete user."))?; Ok(()) } diff --git a/src/errors.rs b/src/errors.rs index 7072dab..daca6bb 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,62 +1,87 @@ -use std::io; - use rocket::{ http::Status, request::Request, - response::{self, Responder, Response}, + response::{self, Responder}, + serde::json::json, }; #[derive(Debug)] -pub enum RBError +pub enum RbError { - /// When the login requests an unknown user - UnknownUser, - BlockedUser, - /// Invalid login password. - InvalidPassword, - /// When a non-admin user tries to use an admin endpoint - Unauthorized, - /// When an expired JWT token is used for auth. - JWTTokenExpired, - /// Umbrella error for when something goes wrong whilst creating a JWT token pair - JWTCreationError, - JWTError, - MissingJWTKey, - PWSaltError, - AdminCreationError, - TokenExpired, - InvalidRefreshToken, - DuplicateRefreshToken, - DBError, - DuplicateUser, + AuthUnknownUser, + AuthBlockedUser, + AuthInvalidPassword, + AuthUnauthorized, + AuthTokenExpired, + AuthRefreshTokenExpired, + AuthInvalidRefreshToken, + AuthDuplicateRefreshToken, + + // UM = User Management + UMDuplicateUser, + UMUnknownUser, + + DbError(&'static str), + Custom(&'static str), } -impl<'r> Responder<'r, 'static> for RBError +impl RbError { - fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> + pub fn status(&self) -> Status { - let (status, message): (Status, &str) = match self { - RBError::UnknownUser => (Status::NotFound, "Unknown user"), - RBError::BlockedUser => (Status::Unauthorized, "This user is blocked"), - RBError::InvalidPassword => (Status::Unauthorized, "Invalid password"), - RBError::Unauthorized => (Status::Unauthorized, "Unauthorized"), - RBError::JWTTokenExpired => (Status::Unauthorized, "Token expired"), - RBError::JWTCreationError | RBError::MissingJWTKey => { - (Status::InternalServerError, "Failed to create tokens.") - } - RBError::InvalidRefreshToken | RBError::DuplicateRefreshToken => { - (Status::Unauthorized, "Invalid refresh token.") - } - RBError::DuplicateUser => (Status::Conflict, "User already exists"), - _ => (Status::InternalServerError, "Internal server error"), - }; + // Every entry gets its own line for easy editing later when needed + match self { + RbError::AuthUnknownUser => Status::NotFound, + RbError::AuthBlockedUser => Status::Forbidden, + RbError::AuthInvalidPassword => Status::Unauthorized, + RbError::AuthUnauthorized => Status::Unauthorized, + RbError::AuthTokenExpired => Status::Unauthorized, + RbError::AuthRefreshTokenExpired => Status::Unauthorized, + RbError::AuthInvalidRefreshToken => Status::Unauthorized, + RbError::AuthDuplicateRefreshToken => Status::Unauthorized, - let mut res = Response::new(); - res.set_status(status); - res.set_sized_body(message.len(), io::Cursor::new(message)); + RbError::UMDuplicateUser => Status::Conflict, - Ok(res) + RbError::Custom(_) => Status::InternalServerError, + _ => Status::InternalServerError, + } + } + + pub fn message(&self) -> &'static str + { + match self { + RbError::AuthUnknownUser => "This user doesn't exist.", + RbError::AuthBlockedUser => "This user is blocked.", + RbError::AuthInvalidPassword => "Invalid credentials.", + RbError::AuthUnauthorized => "You are not authorized to access this resource.", + RbError::AuthTokenExpired => "This token is not valid anymore.", + RbError::AuthRefreshTokenExpired => "This refresh token is not valid anymore.", + RbError::AuthInvalidRefreshToken => "This refresh token is not valid.", + RbError::AuthDuplicateRefreshToken => { + "This refresh token has already been used. The user has been blocked." + } + + RbError::UMDuplicateUser => "This user already exists.", + + RbError::Custom(message) => message, + _ => "", + } } } -pub type Result = std::result::Result; +impl<'r> Responder<'r, 'static> for RbError +{ + fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> + { + let status = self.status(); + let content = json!({ + "status": status.code, + "message": self.message(), + }); + + // TODO add status to response + content.respond_to(req) + } +} + +pub type Result = std::result::Result; diff --git a/src/guards.rs b/src/guards.rs index 2994313..55df193 100644 --- a/src/guards.rs +++ b/src/guards.rs @@ -1,6 +1,6 @@ use hmac::{Hmac, NewMac}; use jwt::VerifyWithKey; -use rb::auth::Claims; +use rb::auth::jwt::Claims; use rocket::{ http::Status, outcome::try_outcome, @@ -14,7 +14,7 @@ pub struct Bearer<'a>(&'a str); #[rocket::async_trait] impl<'r> FromRequest<'r> for Bearer<'r> { - type Error = rb::errors::RBError; + type Error = rb::errors::RbError; async fn from_request(req: &'r Request<'_>) -> Outcome { @@ -39,12 +39,12 @@ impl<'r> FromRequest<'r> for Bearer<'r> } /// Verifies the provided JWT is valid. -pub struct JWT(Claims); +pub struct Jwt(Claims); #[rocket::async_trait] -impl<'r> FromRequest<'r> for JWT +impl<'r> FromRequest<'r> for Jwt { - type Error = rb::errors::RBError; + type Error = rb::errors::RbError; async fn from_request(req: &'r Request<'_>) -> Outcome { @@ -54,19 +54,27 @@ impl<'r> FromRequest<'r> for JWT let secret = match std::env::var("JWT_KEY") { Ok(key) => key, Err(_) => { - return Outcome::Failure((Status::InternalServerError, Self::Error::MissingJWTKey)) + return Outcome::Failure(( + Status::InternalServerError, + Self::Error::AuthUnauthorized, + )) } }; let key: Hmac = match Hmac::new_from_slice(secret.as_bytes()) { Ok(key) => key, Err(_) => { - return Outcome::Failure((Status::InternalServerError, Self::Error::JWTError)) + return Outcome::Failure(( + Status::InternalServerError, + Self::Error::Custom("Failed to do Hmac thing."), + )) } }; // Verify token using key let claims: Claims = match bearer.verify_with_key(&key) { Ok(claims) => claims, - Err(_) => return Outcome::Failure((Status::Unauthorized, Self::Error::Unauthorized)), + Err(_) => { + return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)) + } }; Outcome::Success(Self(claims)) @@ -79,15 +87,15 @@ pub struct User(Claims); #[rocket::async_trait] impl<'r> FromRequest<'r> for User { - type Error = rb::errors::RBError; + type Error = rb::errors::RbError; async fn from_request(req: &'r Request<'_>) -> Outcome { - let claims = try_outcome!(req.guard::().await).0; + let claims = try_outcome!(req.guard::().await).0; // Verify key hasn't yet expired if chrono::Utc::now().timestamp() > claims.exp { - return Outcome::Failure((Status::Forbidden, Self::Error::TokenExpired)); + return Outcome::Failure((Status::Forbidden, Self::Error::AuthTokenExpired)); } Outcome::Success(Self(claims)) @@ -100,7 +108,7 @@ pub struct Admin(Claims); #[rocket::async_trait] impl<'r> FromRequest<'r> for Admin { - type Error = rb::errors::RBError; + type Error = rb::errors::RbError; async fn from_request(req: &'r Request<'_>) -> Outcome { diff --git a/src/routes/admin.rs b/src/routes/admin.rs index b3db29b..9da75f5 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -1,10 +1,4 @@ -use rb::{ - db::{ - users as db_users, - users::{NewUser, User}, - }, - errors::RBError, -}; +use rb::{db, errors::RbError}; use rocket::serde::json::Json; use uuid::Uuid; @@ -12,30 +6,34 @@ use crate::{guards::Admin, RbDbConn}; pub fn routes() -> Vec { - routes![get_users, get_user_info] + routes![get_users, get_user_info, create_user] } #[get("/users")] -async fn get_users(admin: Admin, conn: RbDbConn) -> rb::Result>> +async fn get_users(_admin: Admin, conn: RbDbConn) -> rb::Result>> { - Ok(Json(conn.run(|c| rb::db::users::all(c)).await?)) + Ok(Json(conn.run(|c| db::users::all(c)).await?)) } #[post("/users", data = "")] -async fn create_user(admin: Admin, conn: RbDbConn, user: Json) -> rb::Result<()> +async fn create_user(_admin: Admin, conn: RbDbConn, user: Json) -> rb::Result<()> { Ok(conn - .run(move |c| db_users::create(c, &user.into_inner())) + .run(move |c| db::users::create(c, &user.into_inner())) .await?) } #[get("/users/")] -async fn get_user_info(_admin: Admin, conn: RbDbConn, user_id_str: &str) -> rb::Result> +async fn get_user_info( + _admin: Admin, + conn: RbDbConn, + user_id_str: &str, +) -> rb::Result> { - let user_id = Uuid::parse_str(user_id_str).map_err(|_| RBError::UnknownUser)?; + let user_id = Uuid::parse_str(user_id_str).map_err(|_| RbError::UMUnknownUser)?; - match conn.run(move |c| db_users::find(c, user_id)).await { + match conn.run(move |c| db::users::find(c, user_id)).await { Some(user) => Ok(Json(user)), - None => Err(RBError::UnknownUser), + None => Err(RbError::UMUnknownUser), } } diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 9551ace..955cfaa 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,4 +1,7 @@ -use rb::auth::{generate_jwt_token, verify_user, JWTResponse}; +use rb::auth::{ + jwt::{generate_jwt_token, JWTResponse}, + verify_user, +}; use rocket::serde::json::Json; use serde::Deserialize; @@ -51,7 +54,7 @@ async fn refresh_token( let refresh_token = refresh_token_request.into_inner().refresh_token; Ok(Json( - conn.run(move |c| rb::auth::refresh_token(c, &refresh_token)) + conn.run(move |c| rb::auth::jwt::refresh_token(c, &refresh_token)) .await?, )) }