Working with httpauth, trying to use extractors

This commit is contained in:
Benjamin Sherriff
2023-10-18 10:24:43 -04:00
parent d3965efd28
commit d245e41978
9 changed files with 199 additions and 209 deletions

79
service/src/auth/mod.rs Normal file
View File

@@ -0,0 +1,79 @@
use std::env;
use actix_web::{dev::ServiceRequest, Error as ActixError};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use argon2::{password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString, Error as HashError}, Argon2, PasswordHash};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm};
use serde::{Deserialize, Serialize};
use siren::ServiceError;
mod model;
mod routes;
pub use model::*;
pub use routes::init_routes;
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
exp: usize,
}
// 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, (ActixError, ServiceRequest)> {
let token = credentials.token();
match validate_token(token) {
Ok(_) => Ok(req),
Err(err) => {
Err((ActixError::from(actix_web::error::ErrorUnauthorized(err)), req))
}
}
}
fn validate_token(token: &str) -> Result<(), ServiceError> {
let jwt_secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
match decode::<Claims>(token, &DecodingKey::from_secret(jwt_secret.as_ref()), &Validation::new(Algorithm::HS256)) {
Ok(token_data) => {
println!("{:?}", token_data.claims);
if token_data.claims.exp < chrono::Utc::now().timestamp() as usize {
return Err(ServiceError {
status: 401,
message: "Token expired".to_string()
});
}
Ok(())
},
Err(err) => {
Err(ServiceError {
status: 401,
message: err.to_string()
})
}
}
}
pub fn create_token(email: &str) -> String {
let jwt_secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let exp = chrono::Utc::now().checked_add_signed(chrono::Duration::seconds(3600)).expect("valid timestamp").timestamp();
let claims = Claims {
sub: email.to_owned(),
exp: exp as usize,
};
let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(jwt_secret.as_ref())).unwrap();
token
}
pub fn hash_password(password: &[u8]) -> Result<String, HashError> {
let salt = SaltString::generate(&mut OsRng);
Ok(Argon2::default().hash_password(password, &salt)?.to_string())
}
pub fn verify_password(hash: &str, password: &[u8]) -> Result<(), HashError> {
let parsed_hash = PasswordHash::new(hash)?;
Ok(Argon2::default().verify_password(password, &parsed_hash)?)
}

81
service/src/auth/model.rs Normal file
View File

@@ -0,0 +1,81 @@
use diesel::prelude::*;
use serde::{Serialize, Deserialize};
use siren::ServiceError;
use crate::db::schema::users;
use super::hash_password;
#[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_password(self.password.as_bytes())?;
Ok(InsertUser {
email: self.email.to_lowercase(),
hash,
role: "user".to_string(),
first_name: self.first_name,
last_name: self.last_name,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginRequest {
pub email: String,
pub password: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginResponse {
pub token: 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()?;
// Check if the user exists by email, case insensitive
let user = users::table
.filter(users::email.eq(email.to_lowercase()))
.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)
}
}

View File

@@ -0,0 +1,73 @@
use actix_web::{get, post, web, HttpResponse, ResponseError};
use actix_web_httpauth::middleware::HttpAuthentication;
use siren::ServiceError;
use crate::auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, create_token, LoginResponse};
use super::validator;
#[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(request: web::Json<LoginRequest>) -> HttpResponse {
let email = request.email.clone();
let query_user = match QueryUser::get_by_email(&email) {
Ok(query_user) => query_user,
Err(err) => return ResponseError::error_response(&err)
};
let hash = query_user.hash;
let password = request.password.as_bytes();
match verify_password(&hash, password) {
Ok(_) => {
let token = create_token(&email);
HttpResponse::Ok().json(LoginResponse { token })
},
Err(err) => ResponseError::error_response(&ServiceError {
status: 401,
message: err.to_string()
})
}
}
#[post("/logout")]
async fn logout() -> HttpResponse {
HttpResponse::Ok().finish()
}
#[get("/ping")]
async fn ping() -> HttpResponse {
HttpResponse::Ok().finish()
}
pub fn init_routes(config: &mut web::ServiceConfig) {
let auth = HttpAuthentication::bearer(validator);
config.service(web::scope("auth")
.service(register)
.service(login)
.service(web::scope("")
.wrap(auth)
.service(logout)
.service(ping)
));
}