diff --git a/service/src/auth/mod.rs b/service/src/auth/mod.rs index 18bff39..8968b48 100644 --- a/service/src/auth/mod.rs +++ b/service/src/auth/mod.rs @@ -1,3 +1,5 @@ +use std::env; + use argon2::{password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString, Error as HashError}, Argon2, PasswordHash}; use base64::{engine::general_purpose, Engine as _}; use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm}; @@ -41,6 +43,26 @@ pub fn verify_token(token: &str, public_key: &str) -> Result Result { + let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE") + .expect("ACCESS_TOKEN_MAXAGE must be set") + .parse::() + .expect("ACCESS_TOKEN_MAXAGE must be an integer"); + let access_private_key = env::var("ACCESS_TOKEN_PRIVATE_KEY") + .expect("ACCESS_TOKEN_PRIVATE_KEY must be set"); + generate_token(&email, access_token_max_age, &access_private_key) +} + +pub fn generate_refresh_token(email: &str) -> Result { + let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE") + .expect("REFRESH_TOKEN_MAXAGE must be set") + .parse::() + .expect("REFRESH_TOKEN_MAXAGE must be an integer"); + let refresh_private_key = env::var("REFRESH_TOKEN_PRIVATE_KEY") + .expect("REFRESH_TOKEN_PRIVATE_KEY must be set"); + generate_token(&email, refresh_token_max_age, &refresh_private_key) +} + pub fn generate_token(email: &str, ttl: i64, private_key: &str) -> Result { let now = chrono::Utc::now(); let mut token_details = TokenDetails { diff --git a/service/src/auth/model.rs b/service/src/auth/model.rs index c6cd5ea..9041f69 100644 --- a/service/src/auth/model.rs +++ b/service/src/auth/model.rs @@ -100,9 +100,8 @@ impl From for ResponseUser { #[derive(Debug, Serialize, Deserialize)] pub struct JwtAuth { - pub access_token_uuid: uuid::Uuid, - pub email: String, - pub role: String, + pub token: uuid::Uuid, + pub user: ResponseUser } impl FromRequest for JwtAuth { @@ -162,7 +161,7 @@ impl FromRequest for JwtAuth { match QueryUser::get_by_email(&user_email) { Ok(user) => { - ready(Ok(JwtAuth { access_token_uuid, email: user.email, role: user.role })) + ready(Ok(JwtAuth { token: access_token_uuid, user: user.into() })) } Err(err) => return ready(Err(ActixError::from(ServiceError { status: 500, diff --git a/service/src/auth/routes.rs b/service/src/auth/routes.rs index 54cba8e..d5588c9 100644 --- a/service/src/auth/routes.rs +++ b/service/src/auth/routes.rs @@ -5,7 +5,7 @@ use log::error; use redis::AsyncCommands; use siren::ServiceError; -use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, generate_token, JwtAuth, ResponseUser}, db}; +use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, JwtAuth, verify_token, generate_access_token, generate_refresh_token}, db}; #[post("/register")] async fn register(user: web::Json) -> HttpResponse { @@ -37,30 +37,19 @@ async fn login(request: web::Json) -> HttpResponse { Ok(query_user) => query_user, Err(err) => return ResponseError::error_response(&err) }; - let hash = query_user.hash; + let hash = &query_user.hash; let password = request.password.as_bytes(); - match verify_password(&hash, password) { + match verify_password(hash, password) { Ok(_) => { - let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE") - .expect("ACCESS_TOKEN_MAXAGE must be set") - .parse::() - .expect("ACCESS_TOKEN_MAXAGE must be an integer"); - let access_private_key = env::var("ACCESS_TOKEN_PRIVATE_KEY") - .expect("ACCESS_TOKEN_PRIVATE_KEY must be set"); - let access_token_details = match generate_token(&email, access_token_max_age, &access_private_key) { + let access_token_details = match generate_access_token(&email) { Ok(token_details) => token_details, Err(err) => { error!("Failed to generate access token: {}", err); return ResponseError::error_response(&err) } }; - let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE") - .expect("REFRESH_TOKEN_MAXAGE must be set") - .parse::() - .expect("REFRESH_TOKEN_MAXAGE must be an integer"); - let refresh_private_key = env::var("REFRESH_TOKEN_PRIVATE_KEY") - .expect("REFRESH_TOKEN_PRIVATE_KEY must be set"); - let refresh_token_details = match generate_token(&email, refresh_token_max_age, &refresh_private_key) { + + let refresh_token_details = match generate_refresh_token(&email) { Ok(token_details) => token_details, Err(err) => { error!("Failed to generate refresh token: {}", err); @@ -76,6 +65,16 @@ async fn login(request: web::Json) -> HttpResponse { } }; + let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE") + .expect("ACCESS_TOKEN_MAXAGE must be set") + .parse::() + .expect("ACCESS_TOKEN_MAXAGE must be an integer"); + + let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE") + .expect("REFRESH_TOKEN_MAXAGE must be set") + .parse::() + .expect("REFRESH_TOKEN_MAXAGE must be an integer"); + let access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &email, (access_token_max_age * 60) as usize).await; if let Err(err) = access_result { error!("Failed to set access token in redis: {}", err); @@ -110,11 +109,13 @@ async fn login(request: web::Json) -> HttpResponse { .http_only(false) .finish(); + let access_token_uuid = uuid::Uuid::parse_str(&access_token_details.token_uuid.to_string()).unwrap(); + HttpResponse::Ok() .cookie(access_cookie) .cookie(refresh_cookie) .cookie(logged_in_cookie) - .json(access_token_details.token.unwrap()) + .json(JwtAuth { token: access_token_uuid, user: query_user.into() }) }, Err(err) => ResponseError::error_response(&ServiceError { status: 401, @@ -125,7 +126,79 @@ async fn login(request: web::Json) -> HttpResponse { #[get("/refresh")] async fn refresh(req: HttpRequest) -> HttpResponse { - HttpResponse::Ok().finish() + let refresh_token = match req.cookie("refresh_token") { + Some(cookie) => cookie.value().to_string(), + None => return ResponseError::error_response(&ServiceError { + status: 401, + message: "Refresh token not found".to_string() + }) + }; + let public_key = env::var("REFRESH_TOKEN_PUBLIC_KEY") + .expect("REFRESH_TOKEN_PUBLIC_KEY must be set"); + let refresh_token_details = match verify_token(&refresh_token, &public_key) { + Ok(token_details) => token_details, + Err(err) => return ResponseError::error_response(&err) + }; + + let mut conn = match db::redis_async_connection().await { + Ok(conn) => conn, + Err(err) => { + error!("Failed to get redis connection: {}", err); + return ResponseError::error_response(&err) + } + }; + + let redis_result: redis::RedisResult = conn.get(refresh_token_details.token_uuid.to_string()).await; + let email = match redis_result { + Ok(email) => email, + Err(err) => return ResponseError::error_response(&ServiceError { + status: 500, + message: format!("Failed to get refresh token from redis: {}", err) + }) + }; + + match QueryUser::get_by_email(&email) { + Ok(_) => { + let access_token_details = match generate_access_token(&email) { + Ok(token_details) => token_details, + Err(err) => { + error!("Failed to generate access token: {}", err); + return ResponseError::error_response(&err) + } + }; + + let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE") + .expect("ACCESS_TOKEN_MAXAGE must be set") + .parse::() + .expect("ACCESS_TOKEN_MAXAGE must be an integer"); + + let access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &email, (access_token_max_age * 60) as usize).await; + if let Err(err) = access_result { + error!("Failed to set access token in redis: {}", err); + return ResponseError::error_response(&ServiceError { + status: 500, + message: format!("Failed to set access token in redis: {}", err) + }) + }; + + let access_cookie = Cookie::build("access_token", access_token_details.token.clone().unwrap()) + .path("/") + .max_age(Duration::new(access_token_max_age * 60, 0)) + .http_only(true) + .finish(); + let logged_in_cookie = Cookie::build("logged_in", "true") + .path("/") + .max_age(Duration::new(access_token_max_age * 60, 0)) + .http_only(false) + .finish(); + + HttpResponse::Ok() + .cookie(access_cookie) + .cookie(logged_in_cookie) + .json(access_token_details.token.unwrap()) + }, + Err(err) => return ResponseError::error_response(&err) + } } #[post("/logout")] @@ -135,12 +208,7 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { #[get("/me")] async fn me(auth: JwtAuth) -> HttpResponse { - let query_user = match QueryUser::get_by_email(&auth.email) { - Ok(user) => user, - Err(err) => return ResponseError::error_response(&err) - }; - let user: ResponseUser = query_user.into(); - HttpResponse::Ok().json(user) + HttpResponse::Ok().json(auth) } pub fn init_routes(config: &mut web::ServiceConfig) { diff --git a/ui/src/api/auth.types.ts b/ui/src/api/auth.types.ts index 7635eea..a6b528d 100644 --- a/ui/src/api/auth.types.ts +++ b/ui/src/api/auth.types.ts @@ -1,3 +1,8 @@ +export interface ResponseUser { + token: string; + user: User; +} + export interface User { email: string; role: string; diff --git a/ui/src/components/Topbar/index.tsx b/ui/src/components/Topbar/index.tsx index 1cb89fe..d8d135d 100644 --- a/ui/src/components/Topbar/index.tsx +++ b/ui/src/components/Topbar/index.tsx @@ -75,7 +75,7 @@ export default function Topbar() { if (Cookies.get('logged_in')) { me().then((response) => { if (response?.status == 200) { - setUser(response.data); + setUser(response.data.user); } }); } else {