diff --git a/src/auth/jwt.rs b/src/auth.rs similarity index 57% rename from src/auth/jwt.rs rename to src/auth.rs index 8767bb3..efd991d 100644 --- a/src/auth/jwt.rs +++ b/src/auth.rs @@ -1,3 +1,4 @@ +use argon2::verify_encoded; use chrono::Utc; use diesel::{insert_into, prelude::*, PgConnection}; use hmac::{Hmac, NewMac}; @@ -9,12 +10,31 @@ use sha2::Sha256; use crate::{ db::{ tokens::{NewRefreshToken, RefreshToken}, - users::User, + users::{NewUser, 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 @@ -34,9 +54,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::Custom("Missing JWT key."))?; - let key: Hmac = Hmac::new_from_slice(secret.as_bytes()) - .map_err(|_| RbError::Custom("Couldn't create Hmac key."))?; + 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 current_time = Utc::now(); @@ -51,7 +71,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::AuthInvalidRefreshToken)?; + let token_bytes = base64::decode(refresh_token).map_err(|_| RBError::InvalidRefreshToken)?; // 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::AuthInvalidRefreshToken)?; + .map_err(|_| RBError::InvalidRefreshToken)?; // If we see that the token has already been used before, we block the user. if token_entry.last_used_at.is_some() { @@ -94,16 +145,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::Custom("Couldn't block user."))?; + .map_err(|_| RBError::DBError)?; - return Err(RbError::AuthDuplicateRefreshToken); + return Err(RBError::DuplicateRefreshToken); } // 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::AuthTokenExpired); + return Err(RBError::TokenExpired); } // We update the last_used_at value for the refresh token @@ -111,7 +162,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::Custom("Couldn't update last used time."))?; + .map_err(|_| RBError::DBError)?; generate_jwt_token(conn, &user) } diff --git a/src/auth/mod.rs b/src/auth/mod.rs deleted file mode 100644 index b315e82..0000000 --- a/src/auth/mod.rs +++ /dev/null @@ -1,63 +0,0 @@ -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 9c831dd..bf3b714 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,5 +1,2 @@ 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 79d337a..b005295 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,9 +30,7 @@ pub struct NewUser pub fn all(conn: &PgConnection) -> crate::Result> { - users - .load::(conn) - .map_err(|_| RbError::DbError("Couldn't get all users.")) + users.load::(conn).map_err(|_| RBError::DBError) } pub fn find(conn: &PgConnection, user_id: Uuid) -> Option @@ -45,10 +43,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("Couldn't create user."))?; + .map_err(|_| RBError::DBError)?; if count == 0 { - return Err(RbError::UMDuplicateUser); + return Err(RBError::DuplicateUser); } Ok(()) @@ -58,7 +56,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("Couldn't delete user."))?; + .map_err(|_| RBError::DBError)?; Ok(()) } diff --git a/src/errors.rs b/src/errors.rs index daca6bb..7072dab 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,87 +1,62 @@ +use std::io; + use rocket::{ http::Status, request::Request, - response::{self, Responder}, - serde::json::json, + response::{self, Responder, Response}, }; #[derive(Debug)] -pub enum RbError +pub enum RBError { - AuthUnknownUser, - AuthBlockedUser, - AuthInvalidPassword, - AuthUnauthorized, - AuthTokenExpired, - AuthRefreshTokenExpired, - AuthInvalidRefreshToken, - AuthDuplicateRefreshToken, - - // UM = User Management - UMDuplicateUser, - UMUnknownUser, - - DbError(&'static str), - Custom(&'static str), + /// 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, } -impl RbError +impl<'r> Responder<'r, 'static> for RBError { - pub fn status(&self) -> Status + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { - // 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, - - RbError::UMDuplicateUser => Status::Conflict, - - 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." + 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"), + }; - RbError::UMDuplicateUser => "This user already exists.", + let mut res = Response::new(); + res.set_status(status); + res.set_sized_body(message.len(), io::Cursor::new(message)); - RbError::Custom(message) => message, - _ => "", - } + Ok(res) } } -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; +pub type Result = std::result::Result; diff --git a/src/guards.rs b/src/guards.rs index 55df193..2994313 100644 --- a/src/guards.rs +++ b/src/guards.rs @@ -1,6 +1,6 @@ use hmac::{Hmac, NewMac}; use jwt::VerifyWithKey; -use rb::auth::jwt::Claims; +use rb::auth::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,27 +54,19 @@ 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::AuthUnauthorized, - )) + return Outcome::Failure((Status::InternalServerError, Self::Error::MissingJWTKey)) } }; let key: Hmac = match Hmac::new_from_slice(secret.as_bytes()) { Ok(key) => key, Err(_) => { - return Outcome::Failure(( - Status::InternalServerError, - Self::Error::Custom("Failed to do Hmac thing."), - )) + return Outcome::Failure((Status::InternalServerError, Self::Error::JWTError)) } }; // Verify token using key let claims: Claims = match bearer.verify_with_key(&key) { Ok(claims) => claims, - Err(_) => { - return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)) - } + Err(_) => return Outcome::Failure((Status::Unauthorized, Self::Error::Unauthorized)), }; Outcome::Success(Self(claims)) @@ -87,15 +79,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::AuthTokenExpired)); + return Outcome::Failure((Status::Forbidden, Self::Error::TokenExpired)); } Outcome::Success(Self(claims)) @@ -108,7 +100,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 9da75f5..b3db29b 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -1,4 +1,10 @@ -use rb::{db, errors::RbError}; +use rb::{ + db::{ + users as db_users, + users::{NewUser, User}, + }, + errors::RBError, +}; use rocket::serde::json::Json; use uuid::Uuid; @@ -6,34 +12,30 @@ use crate::{guards::Admin, RbDbConn}; pub fn routes() -> Vec { - routes![get_users, get_user_info, create_user] + routes![get_users, get_user_info] } #[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| db::users::all(c)).await?)) + Ok(Json(conn.run(|c| rb::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::UMUnknownUser)?; + let user_id = Uuid::parse_str(user_id_str).map_err(|_| RBError::UnknownUser)?; - 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::UMUnknownUser), + None => Err(RBError::UnknownUser), } } diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 955cfaa..9551ace 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,7 +1,4 @@ -use rb::auth::{ - jwt::{generate_jwt_token, JWTResponse}, - verify_user, -}; +use rb::auth::{generate_jwt_token, verify_user, JWTResponse}; use rocket::serde::json::Json; use serde::Deserialize; @@ -54,7 +51,7 @@ async fn refresh_token( let refresh_token = refresh_token_request.into_inner().refresh_token; Ok(Json( - conn.run(move |c| rb::auth::jwt::refresh_token(c, &refresh_token)) + conn.run(move |c| rb::auth::refresh_token(c, &refresh_token)) .await?, )) }