Working on auth

This commit is contained in:
Benjamin Sherriff
2023-10-17 20:49:27 -04:00
parent 140488c925
commit 3b15f520c8
18 changed files with 454 additions and 49 deletions

View File

@@ -37,4 +37,14 @@ diesel::table! {
bot_id -> BigInt,
volume -> Integer,
}
}
diesel::table! {
users (email) {
email -> Text,
hash -> Text,
role -> Text,
first_name -> Text,
last_name -> Text,
}
}

View File

@@ -1,3 +1,5 @@
mod model;
mod routes;
pub use model::*;
pub use routes::init_routes;

View File

@@ -1,37 +1,140 @@
use actix_web::{dev::ServiceRequest, Error};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use std::future::{ready, Ready};
use actix_identity::Identity;
use actix_web::{FromRequest, Error as ActixError, HttpRequest, dev::Payload};
use argon2::{password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString, Error as HashError}, Argon2, PasswordHash};
use diesel::prelude::*;
use serde::{Serialize, Deserialize};
use siren::ServiceError;
pub struct User {
pub id: i32,
use crate::db::schema::users;
#[derive(Debug, Serialize, Deserialize)]
pub struct RegisterUser {
pub email: String,
pub password: String,
pub first_name: String,
pub last_name: String,
}
impl RegisterUser {
pub fn convert_to_insert(self) -> Result<InsertUser, ServiceError> {
let hash = hash(self.password.as_bytes())?;
Ok(InsertUser {
email: self.email,
hash,
role: "user".to_string(),
first_name: self.first_name,
last_name: self.last_name,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginAuth {
pub email: String,
pub password: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoggedUser {
pub email: String
}
impl FromRequest for LoggedUser {
type Error = ActixError;
type Future = Ready<Result<LoggedUser, ActixError>>;
fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
if let Ok(identity) = Identity::from_request(req, pl).into_inner() {
if let Ok(user_json) = identity.id() {
if let Ok(user) = serde_json::from_str(&user_json) {
return ready(Ok(user));
}
}
}
std::future::ready(Err(
ActixError::from(ServiceError {
status: 401,
message: "Unauthorized".to_string(),
})
))
}
}
#[derive(Debug, Queryable, QueryableByName, Serialize, Deserialize)]
#[diesel(table_name = users)]
pub struct QueryUser {
pub email: String,
pub hash: String,
pub role: String,
pub first_name: String,
pub last_name: String,
}
impl QueryUser {
pub fn get_by_email(email: &str) -> Result<QueryUser, ServiceError> {
let mut conn = crate::db::connection()?;
let user = users::table
.filter(users::email.eq(email))
.first(&mut conn)?;
Ok(user)
}
}
#[derive(Debug, Insertable, AsChangeset, Serialize, Deserialize)]
#[diesel(table_name = users)]
pub struct InsertUser {
pub email: String,
pub hash: String,
pub role: String,
pub first_name: String,
pub last_name: String,
}
impl InsertUser {
pub fn insert(user: Self) -> Result<QueryUser, ServiceError> {
let mut conn = crate::db::connection()?;
let user = diesel::insert_into(users::table)
.values(user)
.get_result(&mut conn)?;
Ok(user)
}
}
// https://github.com/Sirneij/rust-auth/blob/main/backend/src/routes/users/login.rs
// https://dev.to/sirneij/authentication-system-using-rust-actix-web-and-sveltekit-user-registration-580h
// https://github.com/actix/actix-extras/blob/master/actix-session/examples/basic.rs
// maybe https://github.com/actix/actix-extras/blob/master/actix-identity/examples/identity.rs
// https://www.lpalmieri.com/posts/session-based-authentication-in-rust/#3-3-1-postgres
pub async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result<ServiceRequest, (Error, ServiceRequest)> {
let token = credentials.token();
println!("{:?}", req);
match validate_token(token) {
Ok(res) => {
if res {
Ok(req)
} else {
Err((Error::from(actix_web::error::ErrorUnauthorized("Invalid token")), req))
}
},
Err(err) => {
Err((Error::from(actix_web::error::ErrorUnauthorized(err)), req))
}
}
// pub async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result<ServiceRequest, (ActixError, ServiceRequest)> {
// let token = credentials.token();
// println!("{:?}", req);
// match validate_token(token) {
// Ok(res) => {
// if res {
// Ok(req)
// } else {
// Err((ActixError::from(actix_web::error::ErrorUnauthorized("Invalid token")), req))
// }
// },
// Err(err) => {
// Err((ActixError::from(actix_web::error::ErrorUnauthorized(err)), req))
// }
// }
// }
// fn validate_token(token: &str) -> Result<bool, ServiceError> {
// println!("Validating token: {}", token);
// Ok(true)
// }
pub fn hash(password: &[u8]) -> Result<String, HashError> {
let salt = SaltString::generate(&mut OsRng);
Ok(Argon2::default().hash_password(password, &salt)?.to_string())
}
fn validate_token(token: &str) -> Result<bool, ServiceError> {
println!("Validating token: {}", token);
Ok(true)
}
pub fn verify(hash: &str, password: &[u8]) -> Result<(), HashError> {
let parsed_hash = PasswordHash::new(hash)?;
Ok(Argon2::default().verify_password(password, &parsed_hash)?)
}

View File

@@ -0,0 +1,75 @@
use actix_identity::Identity;
use actix_web::{get, post, web, HttpResponse, HttpRequest, ResponseError, HttpMessage};
use siren::ServiceError;
use crate::db::users::{LoginAuth, RegisterUser, InsertUser, QueryUser, verify, LoggedUser};
#[post("/register")]
async fn register(user: web::Json<RegisterUser>) -> HttpResponse {
let register_user = user.0;
let insert_user: InsertUser = match register_user.convert_to_insert() {
Ok(user) => user,
Err(err) => return ResponseError::error_response(&err)
};
match InsertUser::insert(insert_user) {
Ok(_) => {
HttpResponse::Created().finish()
},
Err(err) => {
// Obfuscate the service error message to prevent leaking database details
if err.status == 409 {
return HttpResponse::Conflict().finish();
} else {
return ResponseError::error_response(&err);
}
}
}
}
#[post("/login")]
async fn login(req: HttpRequest, auth: web::Json<LoginAuth>) -> HttpResponse {
let email = auth.email.clone();
match QueryUser::get_by_email(&email) {
Ok(query_user) => {
let hash = query_user.hash;
let password = auth.password.as_bytes();
match verify(&hash, password) {
Ok(_) => {
let user = LoggedUser {
email: email.clone()
};
let user_string = serde_json::to_string(&user).unwrap();
match Identity::login(&req.extensions(), user_string) {
Ok(_) => HttpResponse::Ok().finish(),
Err(err) => return ResponseError::error_response(&err)
}
},
Err(err) => ResponseError::error_response(&ServiceError {
status: 401,
message: err.to_string()
})
}
},
Err(err) => ResponseError::error_response(&err)
}
}
#[post("/logout")]
async fn logout(id: Identity) -> HttpResponse {
id.logout();
HttpResponse::Ok().finish()
}
#[get("/me")]
async fn me(user: LoggedUser) -> HttpResponse {
HttpResponse::Ok().json(user)
}
pub fn init_routes(config: &mut web::ServiceConfig) {
config.service(web::scope("users")
.service(register)
.service(login)
.service(logout)
.service(me));
}