diff --git a/src/types.rs b/src/types.rs index ccc6573..1c4dc62 100644 --- a/src/types.rs +++ b/src/types.rs @@ -28,3 +28,31 @@ pub struct Item { pub image: String, pub price: f64, } + +#[derive(Serialize, Deserialize)] +pub struct UserLogin { + pub username: String, + pub password: String, +} + +#[derive(Serialize, Deserialize)] +pub struct UserRegister { + pub username: String, + pub password: String, + pub email: String, +} + +pub struct User { + pub id: i64, + pub uuid: String, + pub username: String, + pub hash: String, + pub email: String, +} + +#[derive(Serialize, Deserialize)] +pub struct UserTokenClaims { + pub exp: usize, // Required (validate_exp defaults to true in validation). Expiration time (as UTC timestamp) + pub kid: i64, + pub uid: String, +} diff --git a/src/users.rs b/src/users.rs index 9b2b669..090a7ca 100644 --- a/src/users.rs +++ b/src/users.rs @@ -1,177 +1,177 @@ use crate::AppState; -use actix_web::cookie::{Cookie, SameSite}; +use crate::types::{User, UserLogin, UserRegister, UserTokenClaims}; use actix_web::web::{Data, Json}; -use actix_web::{post, HttpRequest, HttpResponse, Responder}; -use argon2::password_hash::{ - rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, -}; +use actix_web::{HttpRequest, HttpResponse, Responder, post}; use argon2::Argon2; +use argon2::password_hash::{ + PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng, +}; use jsonwebtoken::{ - decode, encode, get_current_timestamp, Algorithm, DecodingKey, EncodingKey, Header, Validation, + Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode, get_current_timestamp, }; use rand::Rng; -use serde::{Deserialize, Serialize}; +use serde_json::{json, to_string}; use sqlx::{query, query_as}; +use std::error::Error; use std::fs::File; use std::io::Read; -use actix_web::cookie::time::{Duration, OffsetDateTime, UtcDateTime}; -use serde_json::{json, to_string}; use uuid::Uuid; +/* +Will use cookies in final version of app +In the meanwhile, we'll just save the token in the userStore -#[derive(Serialize, Deserialize)] -struct UserLogin { - username: String, - password: String, -} - -#[derive(Serialize, Deserialize)] -struct UserRegister { - username: String, - password: String, - email: String, -} - -struct User { - id: i64, - uuid: String, - username: String, - hash: String, - email: String, -} - -struct WebUser { - uuid: String, - username: String, -} - -#[derive(Serialize, Deserialize)] -struct UserTokenClaims { - exp: usize, // Required (validate_exp defaults to true in validation). Expiration time (as UTC timestamp) - kid: i64, - uid: String, -} +use actix_web::cookie::{Cookie, SameSite}; +use actix_web::cookie::time::{Duration, OffsetDateTime, UtcDateTime}; +*/ #[post("/login")] -async fn login(user_login: Json, app_state: Data) -> impl Responder { +async fn login( + user_login: Json, + app_state: Data, +) -> Result> { // Verify that the password is correct let argon2 = Argon2::default(); - let user = query_as!( - User, - "SELECT * FROM users WHERE username = $1", - user_login.username + Ok( + match query_as!( + User, + "SELECT * FROM users WHERE username = $1", + user_login.username + ) + .fetch_optional(&app_state.database) + .await? + { + Some(user) => { + let hash = PasswordHash::new(&user.hash)?; + if argon2 + .verify_password(user_login.password.as_bytes(), &hash) + .is_err() + { + return Ok(HttpResponse::BadRequest().finish()); + } + // Create the JWT + let header = Header::new(Algorithm::ES256); + // Put a random KeyId + let mut rng = rand::rng(); + let key_id: i64 = rng.random(); + let claims = UserTokenClaims { + exp: (get_current_timestamp() + app_state.token_expiration) as usize, + kid: key_id, + uid: user.uuid.clone(), + }; + let mut key = File::open("priv.pem")?; + let mut buf = vec![]; + key.read_to_end(&mut buf)?; + let token = encode(&header, &claims, &EncodingKey::from_ec_pem(&buf)?)?; + let user = json!({ + "uuid": user.uuid, + "username": user.username, + "token": token, + }); + let user_string = to_string(&user)?; + // Send the JWT as cookie + HttpResponse::Ok().body(user_string) + } + None => HttpResponse::BadRequest().finish(), + }, ) - .fetch_one(&app_state.database) - .await; - if user.is_err() { - return HttpResponse::BadRequest().finish(); - } - let user = user.unwrap(); - let hash = PasswordHash::new(&user.hash); - if hash.is_err() { - return HttpResponse::BadRequest().finish(); - } - if argon2 - .verify_password(user_login.password.as_bytes(), &hash.unwrap()) - .is_err() - { - return HttpResponse::BadRequest().finish(); - } - // Create the JWT - let header = Header::new(Algorithm::ES256); - // Put a random KeyId - let mut rng = rand::rng(); - let key_id: i64 = rng.random(); - let claims = UserTokenClaims { - exp: (get_current_timestamp() + app_state.token_expiration) as usize, - kid: key_id, - uid: user.uuid.clone(), - }; - let mut key = File::open("priv.pem").unwrap(); - let mut buf = vec![]; - key.read_to_end(&mut buf).unwrap(); - let token = encode(&header, &claims, &EncodingKey::from_ec_pem(&buf).unwrap()).unwrap(); - let user = json!({ - "uuid": user.uuid, - "username": user.username, - "token": token, - }); - // Send the JWT as cookie - HttpResponse::Ok() - .body(to_string(&user).unwrap()) } #[post("/logout")] -async fn logout(req: HttpRequest, app_state: Data) -> impl Responder { +async fn logout( + req: HttpRequest, + app_state: Data, +) -> Result> { // Put the (KeyId, User) pair in the revoked table // And remove data from client - let token = req.headers().get("Authorization"); - if token.is_none() { - return HttpResponse::BadRequest().finish(); - } - let token = token.unwrap(); - let token = token.to_str(); - if token.is_err() { - return HttpResponse::BadRequest().finish(); - } - let token = token.unwrap().split_once(" ").unwrap().1; - let mut key = File::open("pub.pem").unwrap(); - let mut buf = vec![]; - key.read_to_end(&mut buf).unwrap(); - let token = decode::( - token, - &DecodingKey::from_ec_pem(&buf).unwrap(), - &Validation::new(Algorithm::ES256), - ); - match token { - Ok(token) => { + match req.headers().get("Authorization") { + Some(token) => { + let token = token.to_str()?; + let token = match token.split_once(" ") { + Some((_, token)) => token, + None => return Ok(HttpResponse::BadRequest().finish()), + }; + let mut key = File::open("pub.pem")?; + let mut buf = vec![]; + key.read_to_end(&mut buf)?; + let token = decode::( + token, + &DecodingKey::from_ec_pem(&buf).unwrap(), + &Validation::new(Algorithm::ES256), + )?; let exp = token.claims.exp as i64; - if query!( + query!( "INSERT INTO revoked ( token_id, user_id, expires ) VALUES ( $1, $2, $3 )", token.claims.kid, token.claims.uid, exp ) - .execute(&app_state.database) - .await - .is_err() - { - return HttpResponse::InternalServerError() - .reason("Query fail") - .finish(); - } - } - Err(e) => { - let message = format!("Error: {e}"); - println!("{}", message); - return HttpResponse::InternalServerError() - .reason("Token caca") - .finish(); + .execute(&app_state.database) + .await?; + Ok(HttpResponse::Ok().finish()) } + None => Ok(HttpResponse::BadRequest().finish()), } - HttpResponse::Ok().finish() } #[post("/register")] -async fn register(user_register: Json, app_state: Data) -> impl Responder { - let uuid = Uuid::new_v4().to_string(); +async fn register( + user_register: Json, + app_state: Data, +) -> Result> { + let mut uuid = Uuid::new_v4().to_string(); + while query!("SELECT (uuid) FROM users WHERE uuid = $1", uuid) + .fetch_optional(&app_state.database) + .await? + .is_some() + { + uuid = Uuid::new_v4().to_string(); + } let argon2 = Argon2::default(); let salt = SaltString::generate(&mut OsRng); let hash = argon2 - .hash_password(user_register.password.as_bytes(), &salt) - .unwrap() + .hash_password(user_register.password.as_bytes(), &salt)? .to_string(); - if query!( + query!( "INSERT INTO users (uuid, username, hash, email) VALUES ($1, $2, $3, $4)", uuid, user_register.username, hash, user_register.email ) - .execute(&app_state.database) - .await - .is_err() - { - return HttpResponse::InternalServerError().finish(); - }; - HttpResponse::Ok().finish() + .execute(&app_state.database) + .await?; + Ok(HttpResponse::Ok().finish()) +} + +async fn verify_token( + app_state: Data, + token: &str, +) -> Result> { + let mut key = File::open("pub.pem")?; + let mut buf = vec![]; + key.read_to_end(&mut buf)?; + let token = decode::( + token, + &DecodingKey::from_ec_pem(&buf).unwrap(), + &Validation::new(Algorithm::ES256), + )?; + let exp = token.claims.exp as u64; + let now = get_current_timestamp(); + if exp > now { + return Ok(false); + } + let kid = token.claims.kid; + let uid = token.claims.uid; + if query!( + "SELECT token_id FROM revoked WHERE token_id = $1 AND user_id = $2", + kid, + uid + ) + .fetch_optional(&app_state.database) + .await? + .is_some() + { + return Ok(false); + } + Ok(true) }