diff --git a/service/Cargo.toml b/service/Cargo.toml index d8b0224..7054552 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -32,6 +32,7 @@ rust-s3 = "0.33.0" actix-multipart = "0.6.1" openssl = "0.10.60" # Resolve `openssl` `X509StoreRef::objects` is unsound #10 rand = "0.8.5" +sha2 = "0.10.8" [dependencies.tokio] version = "1.32.0" diff --git a/service/src/auth/mod.rs b/service/src/auth/mod.rs index 3fd4ffc..dbc5e5d 100644 --- a/service/src/auth/mod.rs +++ b/service/src/auth/mod.rs @@ -1,148 +1,7 @@ -use std::env; - -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}; - mod model; mod routes; +mod tokens; pub use model::*; +pub use tokens::*; pub use routes::init_routes; -use siren::ServiceError; - -#[derive(Debug, Serialize, Deserialize)] -struct TokenClaims { - sub: String, // Subject (User) - token_uuid: String, // Token UUID - iss: String, // Issuer (Service) - exp: i64, // Expiration time - iat: i64, // Issued At - nbf: i64 // Not Before -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum TokenType { - #[serde(rename = "access")] - Access, - #[serde(rename = "refresh")] - Refresh, - #[serde(rename = "none")] - None -} - -impl std::fmt::Display for TokenType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TokenType::Access => write!(f, "access"), - TokenType::Refresh => write!(f, "refresh"), - TokenType::None => write!(f, "none") - } - } -} - -impl std::str::FromStr for TokenType { - type Err = ServiceError; - - fn from_str(s: &str) -> Result { - match s { - "access" => Ok(TokenType::Access), - "refresh" => Ok(TokenType::Refresh), - "none" => Ok(TokenType::None), - _ => Err(ServiceError::new(400, "Invalid token type".to_string())) - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct TokenDetails { - pub token: Option, - pub token_uuid: uuid::Uuid, - pub email: String, - pub token_type: TokenType, - pub expires_in: Option -} - -impl std::fmt::Display for TokenDetails { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}:{}", self.token_type.to_string(), self.email, self.token_uuid.to_string()) - } -} - -impl std::str::FromStr for TokenDetails { - type Err = ServiceError; - - fn from_str(s: &str) -> Result { - let parts: Vec<&str> = s.split(":").collect(); - if parts.len() != 2 { - return Err(ServiceError::new(400, "Invalid token".to_string())); - } - let token_type = parts[0].parse::()?; - let email = parts[1].to_string(); - let uuid = uuid::Uuid::parse_str(parts[2])?; - Ok(TokenDetails { token: None, token_uuid: uuid, email, token_type, expires_in: None }) - } -} - -pub fn verify_token(token: &str, public_key: &str) -> Result { - let key = DecodingKey::from_rsa_pem(public_key.as_bytes())?; - let validation = Validation::new(Algorithm::RS256); - let decoded = decode::(token, &key, &validation)?; - let email = decoded.claims.sub; - let token_uuid = uuid::Uuid::parse_str(decoded.claims.token_uuid.as_str()).unwrap(); - Ok(TokenDetails { token: None, token_uuid, email, token_type: TokenType::None, expires_in: None }) -} - -pub fn generate_access_token(email: &str) -> 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 keys_dir = env::var("KEYS_DIR_PATH")?; - let access_private_key = std::fs::read_to_string(format!("{}/access_private_key.pem", keys_dir))?; - generate_token(&email, TokenType::Refresh, 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 keys_dir = env::var("KEYS_DIR_PATH")?; - let refresh_private_key = std::fs::read_to_string(format!("{}/refresh_private_key.pem", keys_dir))?; - generate_token(&email, TokenType::Access, refresh_token_max_age, &refresh_private_key) -} - -fn generate_token(email: &str, token_type: TokenType, ttl: i64, private_key: &str) -> Result { - let now = chrono::Utc::now(); - let mut token_details = TokenDetails { - token: None, - token_uuid: uuid::Uuid::new_v4(), - email: email.to_string(), - token_type, - expires_in: Some((now + chrono::Duration::minutes(ttl)).timestamp()) - }; - let claims = TokenClaims { - sub: token_details.email.clone(), - iss: "siren".to_string(), - token_uuid: token_details.token_uuid.to_string(), - exp: token_details.expires_in.unwrap(), - iat: now.timestamp(), - nbf: now.timestamp() - }; - let header = Header::new(Algorithm::RS256); - let key = EncodingKey::from_rsa_pem(private_key.as_bytes())?; - let token = encode(&header, &claims, &key)?; - token_details.token = Some(token); - Ok(token_details) -} - -pub fn hash_password(password: &[u8]) -> Result { - 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)?) -} diff --git a/service/src/auth/model.rs b/service/src/auth/model.rs index 7ecf064..5620a2e 100644 --- a/service/src/auth/model.rs +++ b/service/src/auth/model.rs @@ -1,5 +1,6 @@ use std::{future::{ready, Ready}, env}; use actix_web::{FromRequest, Error as ActixError, HttpRequest, dev::Payload, http}; +use argon2::{password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString, Error as HashError}, Argon2, PasswordHash}; use diesel::prelude::*; use log::error; use redis::Commands; @@ -8,7 +9,7 @@ use siren::ServiceError; use crate::storage::{schema::users, connection}; -use super::{hash_password, verify_token}; +use super::AccessToken; #[derive(Debug, Serialize, Deserialize)] pub struct RegisterUser { @@ -35,6 +36,16 @@ impl RegisterUser { } } +fn hash_password(password: &[u8]) -> Result { + 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)?) +} + #[derive(Debug, Serialize, Deserialize)] pub struct LoginRequest { pub email: String, @@ -131,7 +142,7 @@ impl FromRequest for JwtAuth { type Error = ActixError; type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - let access_token = match req + let access_token_string = match req .cookie("access_token") .map(|c| c.value().to_string()) .or_else(|| { @@ -148,18 +159,18 @@ impl FromRequest for JwtAuth { let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set"); let public_key = std::fs::read_to_string(format!("{}/access_public_key.pem", keys_dir)).expect("Failed to read access public key"); - let access_token_details = match verify_token(&access_token, &public_key) { + let access_token = match AccessToken::decode(&access_token_string, &public_key) { Ok(token_details) => token_details, Err(err) => { error!("Failed to verify access token: {}", err); return ready(Err(ActixError::from(ServiceError { status: 401, - message: format!("Failed to verify access token: {}", err) + message: format!("Access token is invaid: {}", err) }))) } }; - let access_token_uuid = uuid::Uuid::parse_str(&access_token_details.token_uuid.to_string()).unwrap(); + let access_token_uuid = uuid::Uuid::parse_str(&access_token.token_uuid.to_string()).unwrap(); let mut conn = match crate::storage::redis_connection() { Ok(conn) => conn, @@ -172,11 +183,11 @@ impl FromRequest for JwtAuth { } }; let user_email = match conn.get::<_, String>(access_token_uuid.clone().to_string()) { - Ok(result) => result, + Ok(result) => serde_json::from_str::(&result).unwrap().email, Err(_) => { return ready(Err(ActixError::from(ServiceError { status: 401, - message: format!("Access token was not found") + message: format!("Access token is invalid") }))) } }; @@ -187,7 +198,7 @@ impl FromRequest for JwtAuth { } Err(_) => return ready(Err(ActixError::from(ServiceError { status: 401, - message: format!("User was not found") + message: format!("User does not exist") }))) } } diff --git a/service/src/auth/routes.rs b/service/src/auth/routes.rs index efa9fb8..a6a3e0a 100644 --- a/service/src/auth/routes.rs +++ b/service/src/auth/routes.rs @@ -6,7 +6,7 @@ use redis::AsyncCommands; use serde::{Serialize, Deserialize}; use siren::ServiceError; -use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, JwtAuth, verify_token, generate_access_token, generate_refresh_token}, storage}; +use crate::{auth::{verify_password, AccessToken, InsertUser, JwtAuth, LoginRequest, QueryUser, RefreshToken, RegisterUser}, storage}; #[post("/register")] async fn register(user: web::Json) -> HttpResponse { @@ -31,18 +31,20 @@ async fn register(user: web::Json) -> HttpResponse { } #[post("/login")] -async fn login(request: web::Json) -> HttpResponse { - let email = request.email.clone(); +async fn login(request: HttpRequest, login_request: web::Json) -> HttpResponse { + let email = login_request.email.clone(); + // Get IP address + let ip_address = request.peer_addr().unwrap().ip().to_string(); 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(); + let password = login_request.password.as_bytes(); match verify_password(hash, password) { Ok(_) => { - let access_token_details = match generate_access_token(&email) { + let access_token = match AccessToken::new(&email) { Ok(token_details) => token_details, Err(err) => { error!("Failed to generate access token: {}", err); @@ -50,13 +52,7 @@ async fn login(request: web::Json) -> HttpResponse { } }; - let refresh_token_details = match generate_refresh_token(&email) { - Ok(token_details) => token_details, - Err(err) => { - error!("Failed to generate refresh token: {}", err); - return ResponseError::error_response(&err) - } - }; + let refresh_token = RefreshToken::new(&email, &ip_address); let mut conn = match storage::redis_async_connection().await { Ok(conn) => conn, @@ -76,7 +72,7 @@ async fn login(request: web::Json) -> HttpResponse { .parse::() .expect("REFRESH_TOKEN_MAXAGE must be an integer"); - let access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &access_token_details.to_string(), (access_token_max_age * 60) as usize).await; + let access_result: redis::RedisResult<()> = conn.set_ex(access_token.token_uuid.to_string(), &serde_json::to_string(&access_token).unwrap(), (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 { @@ -85,7 +81,7 @@ async fn login(request: web::Json) -> HttpResponse { }) }; - let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token_details.token_uuid.to_string(), &refresh_token_details.to_string(), (refresh_token_max_age * 60) as usize).await; + let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token.hash.to_string(), &serde_json::to_string(&refresh_token).unwrap(), (refresh_token_max_age * 60) as usize).await; if let Err(err) = refresh_result { error!("Failed to set refresh token in redis: {}", err); return ResponseError::error_response(&ServiceError { @@ -94,13 +90,13 @@ async fn login(request: web::Json) -> HttpResponse { }) }; - let access_cookie = Cookie::build("access_token", access_token_details.token.clone().unwrap()) + let access_cookie = Cookie::build("access_token", access_token.token.clone().unwrap()) .path("/") .max_age(Duration::new(access_token_max_age * 60, 0)) .http_only(true) .secure(true) .finish(); - let refresh_cookie = Cookie::build("refresh_token", refresh_token_details.token.clone().unwrap()) + let refresh_cookie = Cookie::build("refresh_token", refresh_token.hash.clone()) .path("/") .max_age(Duration::new(refresh_token_max_age * 60, 0)) .http_only(true) @@ -112,7 +108,7 @@ 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(); + let access_token_uuid = uuid::Uuid::parse_str(&access_token.token_uuid.to_string()).unwrap(); HttpResponse::Ok() .cookie(access_cookie) @@ -134,6 +130,7 @@ struct RefreshParams { #[get("/refresh")] async fn refresh(req: HttpRequest) -> HttpResponse { + let ip_address = req.peer_addr().unwrap().ip().to_string(); let params = match web::Query::::from_query(req.query_string()) { Ok(params) => params, Err(err) => return ResponseError::error_response(&ServiceError { @@ -142,7 +139,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse { }) }; - let refresh_token = match req.cookie("refresh_token") { + let refresh_token_hash = match req.cookie("refresh_token") { Some(cookie) => cookie.value().to_string(), None => return ResponseError::error_response(&ServiceError { status: 401, @@ -150,19 +147,39 @@ async fn refresh(req: HttpRequest) -> HttpResponse { }) }; - let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set"); - let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir)) - .expect("Unable to read refresh public key"); - 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 storage::redis_async_connection().await { + Ok(conn) => conn, + Err(err) => { + error!("Failed to get redis connection: {}", err); + return ResponseError::error_response(&err) + } }; - let email = refresh_token_details.email.clone(); + let refresh_token: RefreshToken = match conn.get::<_, String>(refresh_token_hash.clone()).await { + Ok(result) => match serde_json::from_str(&result) { + Ok(result) => result, + Err(err) => { + error!("Failed to deserialize refresh token: {}", err); + return ResponseError::error_response(&ServiceError { + status: 500, + message: format!("Failed to deserialize refresh token: {}", err) + }) + } + }, + Err(err) => { + error!("Failed to get refresh token from redis: {}", err); + return ResponseError::error_response(&ServiceError { + status: 500, + message: format!("Failed to get refresh token from redis: {}", err) + }) + } + }; + + let email = refresh_token.email.clone(); match QueryUser::get_by_email(&email) { Ok(query_user) => { - let access_token_details = match generate_access_token(&email) { + let access_token_details = match AccessToken::new(&email) { Ok(token_details) => token_details, Err(err) => { error!("Failed to generate access token: {}", err); @@ -170,24 +187,16 @@ async fn refresh(req: HttpRequest) -> HttpResponse { } }; - let mut conn = match storage::redis_async_connection().await { - Ok(conn) => conn, - Err(err) => { - error!("Failed to get redis connection: {}", err); - return ResponseError::error_response(&err) - } - }; - // Delete old access token if it exists match req.cookie("access_token") { Some(cookie) => { let access_token = cookie.value().to_string(); + let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set"); let public_key = std::fs::read_to_string(format!("{}/access_public_key.pem", keys_dir)) .expect("Unable to read access public key"); - match verify_token(&access_token, &public_key) { + match AccessToken::decode(&access_token, &public_key) { Ok(token_details) => { let _: redis::RedisResult<()> = conn.del(token_details.token_uuid.to_string()).await; - }, Err(_) => {} }; @@ -200,7 +209,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse { .parse::() .expect("ACCESS_TOKEN_MAXAGE must be an integer"); - let access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &access_token_details.to_string(), (access_token_max_age * 60) as usize).await; + let access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &serde_json::to_string(&access_token_details).unwrap(), (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 { @@ -230,22 +239,15 @@ async fn refresh(req: HttpRequest) -> HttpResponse { }; if refresh_token_rotation { // Delete the old refresh token - let _: redis::RedisResult<()> = conn.del(refresh_token_details.token_uuid.to_string()).await; + let _: redis::RedisResult<()> = conn.del(refresh_token.hash.to_string()).await; - let refresh_token_details = match generate_refresh_token(&refresh_token_details.email) { - Ok(token_details) => token_details, - Err(err) => { - error!("Failed to generate refresh token: {}", err); - return ResponseError::error_response(&err) - } - }; - + let refresh_token = RefreshToken::new(&refresh_token.email, &ip_address); 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_result: redis::RedisResult<()> = conn.set_ex(refresh_token_details.token_uuid.to_string(), &refresh_token_details.to_string(), (refresh_token_max_age * 60) as usize).await; + let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token.hash.to_string(), &serde_json::to_string(&refresh_token).unwrap(), (refresh_token_max_age * 60) as usize).await; if let Err(err) = refresh_result { error!("Failed to set refresh token in redis: {}", err); return ResponseError::error_response(&ServiceError { @@ -254,7 +256,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse { }) }; - let refresh_cookie = Cookie::build("refresh_token", refresh_token_details.token.clone().unwrap()) + let refresh_cookie = Cookie::build("refresh_token", refresh_token.hash.clone()) .path("/") .max_age(Duration::new(refresh_token_max_age * 60, 0)) .http_only(true) @@ -279,7 +281,6 @@ async fn refresh(req: HttpRequest) -> HttpResponse { #[post("/logout")] async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { - let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set"); let refresh_token = match req.cookie("refresh_token") { Some(cookie) => cookie.value().to_string(), None => return ResponseError::error_response(&ServiceError { @@ -287,12 +288,6 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { message: "Refresh token not found".to_string() }) }; - let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir)) - .expect("Unable to read refresh public key"); - 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 storage::redis_async_connection().await { Ok(conn) => conn, @@ -303,7 +298,7 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { }; let access_result: redis::RedisResult<()> = conn.del(&[ - refresh_token_details.token_uuid.to_string(), + refresh_token.to_string(), auth.token.to_string() ]).await; if let Err(err) = access_result { @@ -342,41 +337,6 @@ async fn me(auth: JwtAuth) -> HttpResponse { HttpResponse::Ok().json(auth) } -#[get("/check-session")] -async fn check_session(req: HttpRequest) -> HttpResponse { - let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set"); - // If there is a access_token cookie, check if it is valid - let has_session = match req.cookie("access_token") { - Some(cookie) => { - let access_token = cookie.value().to_string(); - let public_key = std::fs::read_to_string(format!("{}/access_public_key.pem", keys_dir)) - .expect("Unable to read access public key"); - match verify_token(&access_token, &public_key) { - Ok(_) => true, - Err(_) => false - } - }, - None => false - }; - if !has_session { - // If there is a refresh_token cookie, check if it is valid - match req.cookie("refresh_token") { - Some(cookie) => { - let refresh_token = cookie.value().to_string(); - let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir)) - .expect("Unable to read refresh public key"); - match verify_token(&refresh_token, &public_key) { - Ok(_) => return HttpResponse::Ok().json(true), - Err(_) => return HttpResponse::Ok().json(false) - }; - }, - None => return HttpResponse::Ok().json(false) - }; - } else { - return HttpResponse::Ok().json(true) - } -} - #[get("/roles")] async fn roles() -> HttpResponse { HttpResponse::Ok().json(vec!["admin", "user"]) @@ -401,6 +361,5 @@ pub fn init_routes(config: &mut web::ServiceConfig) { .service(logout) .service(me) .service(roles) - .service(check_session) ); } \ No newline at end of file diff --git a/service/src/auth/tokens.rs b/service/src/auth/tokens.rs new file mode 100644 index 0000000..576c927 --- /dev/null +++ b/service/src/auth/tokens.rs @@ -0,0 +1,87 @@ +use std::env; + +use argon2::password_hash::{rand_core::OsRng, SaltString}; +use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm}; +use serde::{Deserialize, Serialize}; +use sha2::{digest::{generic_array::GenericArray, typenum::U32}, Digest, Sha256}; +use siren::ServiceError; + +#[derive(Debug, Serialize, Deserialize)] +struct TokenClaims { + sub: String, // Subject (User) + token_uuid: String, // Access Token UUID + iss: String, // Issuer (Service) + exp: i64, // Expiration time + iat: i64, // Issued At + nbf: i64 // Not Before +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AccessToken { + pub token: Option, + pub token_uuid: uuid::Uuid, + pub email: String, + pub expires_in: Option +} + +impl AccessToken { + pub fn new(email: &str) -> Result { + let ttl = env::var("ACCESS_TOKEN_MAXAGE") + .expect("ACCESS_TOKEN_MAXAGE must be set") + .parse::() + .expect("ACCESS_TOKEN_MAXAGE must be an integer"); + let keys_dir = env::var("KEYS_DIR_PATH")?; + let private_key = std::fs::read_to_string(format!("{}/access_private_key.pem", keys_dir))?; + let now = chrono::Utc::now(); + let mut token_details = Self { + token: None, + token_uuid: uuid::Uuid::new_v4(), + email: email.to_string(), + expires_in: Some((now + chrono::Duration::minutes(ttl)).timestamp()) + }; + let claims = TokenClaims { + sub: token_details.email.clone(), + iss: "siren".to_string(), + token_uuid: token_details.token_uuid.to_string(), + exp: token_details.expires_in.unwrap(), + iat: now.timestamp(), + nbf: now.timestamp() + }; + let header = Header::new(Algorithm::RS256); + let key = EncodingKey::from_rsa_pem(private_key.as_bytes())?; + let token = encode(&header, &claims, &key)?; + token_details.token = Some(token); + Ok(token_details) + } + + pub fn decode(token: &str, public_key: &str) -> Result { + let key = DecodingKey::from_rsa_pem(public_key.as_bytes())?; + let validation = Validation::new(Algorithm::RS256); + let decoded = decode::(token, &key, &validation)?; + let email = decoded.claims.sub; + let token_uuid = uuid::Uuid::parse_str(decoded.claims.token_uuid.as_str()).unwrap(); + Ok(Self { token: None, token_uuid, email, expires_in: None }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RefreshToken { + pub hash: String, + pub email: String, + pub ip_address: String, + pub timestamp: i64, +} + +impl RefreshToken { + pub fn new(email: &str, ip_address: &str) -> Self { + let now = chrono::Utc::now().timestamp(); + let salt = SaltString::generate(&mut OsRng); + let hash: GenericArray = Sha256::digest(format!("{}:{}:{}:{}", email, ip_address, now, salt).as_bytes()); + Self { + hash: format!("{:x}", hash), + email: email.to_string(), + ip_address: ip_address.to_string(), + timestamp: now + } + } +} \ No newline at end of file diff --git a/ui/src/api/auth.ts b/ui/src/api/auth.ts index 389cc32..319a36b 100644 --- a/ui/src/api/auth.ts +++ b/ui/src/api/auth.ts @@ -43,7 +43,7 @@ export async function me(): Promise { } export async function hasSession(): Promise { - const response = await getRequest('auth/check-session'); + const response = await getRequest('auth/session'); if (response?.status === 200) { return response?.json(); } else {