Working with httpauth, trying to use extractors
This commit is contained in:
79
service/src/auth/mod.rs
Normal file
79
service/src/auth/mod.rs
Normal 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
81
service/src/auth/model.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
73
service/src/auth/routes.rs
Normal file
73
service/src/auth/routes.rs
Normal 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)
|
||||
));
|
||||
}
|
||||
Reference in New Issue
Block a user