From ca9270f3a75499a6524bcd7bc9741818e27c705a Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Tue, 30 Jan 2024 14:19:59 -0500 Subject: [PATCH] Updated files, will be switching to sessions --- README.md | 4 +- generate_keys.sh | 20 ++-- service/Cargo.toml | 1 + service/Dockerfile | 8 +- service/src/auth/model.rs | 26 +--- service/src/auth/routes.rs | 239 +++++++++++++++++-------------------- service/src/auth/tokens.rs | 82 +++++++++---- 7 files changed, 192 insertions(+), 188 deletions(-) diff --git a/README.md b/README.md index de19c10..4350ec8 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,4 @@ docker run --env-file .env -it --rm --name siren siren:latest ``` ### Authentication -The Siren service uses a JWT/session based authentication system, in that JWT tokens are issued and used, but a state is also kept server-side. This is to allow for the ability to revoke and expire tokens, as well as to allow for the ability to have multiple tokens per user. - -Public/Private keys can be generated with `./generate_keys.sh`. These keys should be located within a `/keys` directory in the root of the project. The service's .env file should be updated to reflect the location of the keys. \ No newline at end of file +The Siren service uses a stateful JWT authentication system, which allows for the ability to revoke and expire tokens, as well as to allow for the ability to have multiple tokens per user. A public/private key is needed for the JWT. The keys can be generated with `./generate_keys.sh`. These keys should be located within a `/keys` directory in the root of the project. The `KEYS_DIR_PATH` within the service's .env file should be updated to reflect the location of the keys. \ No newline at end of file diff --git a/generate_keys.sh b/generate_keys.sh index 752eac6..cb3cd86 100755 --- a/generate_keys.sh +++ b/generate_keys.sh @@ -7,17 +7,15 @@ if [ "$#" -eq 1 ]; then fi # Create the keys directory (if it doesn't exist) -echo "Generating keys in: $DIR" +echo "Generating public/private keys in: $DIR" mkdir -p "$DIR" -# Generate Access Keys -openssl genrsa -out $DIR/access_private_key.pem 4096 -openssl rsa -in $DIR/access_private_key.pem -pubout -outform PEM -out $DIR/access_public_key.pem -chmod 600 $DIR/access_private_key.pem -chmod 644 $DIR/access_public_key.pem +# Remove any existing keys +rm -f $DIR/private_key.pem +rm -f $DIR/public_key.pem -# Generate Refresh Keys -openssl genrsa -out $DIR/refresh_private_key.pem 4096 -openssl rsa -in $DIR/refresh_private_key.pem -pubout -outform PEM -out $DIR/refresh_public_key.pem -chmod 600 $DIR/refresh_private_key.pem -chmod 644 $DIR/refresh_public_key.pem +# Generate Keys +openssl genrsa -out $DIR/private_key.pem 4096 +openssl rsa -in $DIR/private_key.pem -pubout -outform PEM -out $DIR/public_key.pem +chmod 600 $DIR/private_key.pem +chmod 644 $DIR/public_key.pem diff --git a/service/Cargo.toml b/service/Cargo.toml index 7054552..b677d7f 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -33,6 +33,7 @@ actix-multipart = "0.6.1" openssl = "0.10.60" # Resolve `openssl` `X509StoreRef::objects` is unsound #10 rand = "0.8.5" sha2 = "0.10.8" +rand_chacha = "0.3.1" [dependencies.tokio] version = "1.32.0" diff --git a/service/Dockerfile b/service/Dockerfile index 763b8cc..3d9cb8e 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -18,10 +18,10 @@ FROM debian:bookworm-slim as keys WORKDIR /keys RUN apt-get update && apt-get install -y openssl libpq-dev -RUN openssl genrsa -out access.pem 4096 -RUN openssl rsa -in access.pem -pubout -outform PEM -out access.pem.pub -RUN openssl genrsa -out refresh.pem 4096 -RUN openssl rsa -in refresh.pem -pubout -outform PEM -out refresh.pem.pub +RUN openssl genrsa -out private_key.pem 4096 +RUN openssl rsa -in private_key.pem -pubout -outform PEM -out public_key.pem +RUN chmod 600 private_key.pem +RUN chmod 644 public_key.pem # ========== # Packages diff --git a/service/src/auth/model.rs b/service/src/auth/model.rs index 5620a2e..86a8ba0 100644 --- a/service/src/auth/model.rs +++ b/service/src/auth/model.rs @@ -1,6 +1,5 @@ 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; @@ -9,7 +8,7 @@ use siren::ServiceError; use crate::storage::{schema::users, connection}; -use super::AccessToken; +use super::{hash, AccessToken}; #[derive(Debug, Serialize, Deserialize)] pub struct RegisterUser { @@ -21,10 +20,9 @@ pub struct RegisterUser { impl RegisterUser { pub fn convert_to_insert(self) -> Result { - let hash = hash_password(self.password.as_bytes())?; Ok(InsertUser { email: self.email.to_lowercase(), - hash, + hash: hash(&self.password)?, role: "user".to_string(), first_name: self.first_name, last_name: self.last_name, @@ -36,16 +34,6 @@ 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, @@ -134,7 +122,7 @@ impl From for ResponseUser { #[derive(Debug, Serialize, Deserialize)] pub struct JwtAuth { - pub token: uuid::Uuid, + pub id: String, pub user: ResponseUser } @@ -157,7 +145,7 @@ 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 public_key = std::fs::read_to_string(format!("{}/public_key.pem", keys_dir)).expect("Failed to read access public key"); let access_token = match AccessToken::decode(&access_token_string, &public_key) { Ok(token_details) => token_details, @@ -169,8 +157,6 @@ impl FromRequest for JwtAuth { }))) } }; - - 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, @@ -182,7 +168,7 @@ impl FromRequest for JwtAuth { }))) } }; - let user_email = match conn.get::<_, String>(access_token_uuid.clone().to_string()) { + let user_email = match conn.get::<_, String>(access_token.id.clone().to_string()) { Ok(result) => serde_json::from_str::(&result).unwrap().email, Err(_) => { return ready(Err(ActixError::from(ServiceError { @@ -194,7 +180,7 @@ impl FromRequest for JwtAuth { match QueryUser::get_by_email(&user_email) { Ok(user) => { - ready(Ok(JwtAuth { token: access_token_uuid, user: user.into() })) + ready(Ok(JwtAuth { id: access_token.id, user: user.into() })) } Err(_) => return ready(Err(ActixError::from(ServiceError { status: 401, diff --git a/service/src/auth/routes.rs b/service/src/auth/routes.rs index a6a3e0a..36cdfbc 100644 --- a/service/src/auth/routes.rs +++ b/service/src/auth/routes.rs @@ -6,7 +6,9 @@ use redis::AsyncCommands; use serde::{Serialize, Deserialize}; use siren::ServiceError; -use crate::{auth::{verify_password, AccessToken, InsertUser, JwtAuth, LoginRequest, QueryUser, RefreshToken, RegisterUser}, storage}; +use crate::{auth::{InsertUser, JwtAuth, LoginRequest, QueryUser, RefreshToken, RegisterUser}, storage}; + +use super::verify_hash; #[post("/register")] async fn register(user: web::Json) -> HttpResponse { @@ -38,87 +40,79 @@ async fn login(request: HttpRequest, login_request: web::Json) -> 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 = login_request.password.as_bytes(); - match verify_password(hash, password) { - Ok(_) => { - let access_token = match AccessToken::new(&email) { - Ok(token_details) => token_details, - Err(err) => { - error!("Failed to generate access 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, - Err(err) => { - error!("Failed to get redis connection: {}", 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 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.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 { - status: 500, - message: format!("Failed to set access token in redis: {}", err) - }) - }; - - 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 { - status: 500, - message: format!("Failed to set refresh token in redis: {}", err) - }) - }; - - 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.hash.clone()) - .path("/") - .max_age(Duration::new(refresh_token_max_age * 60, 0)) - .http_only(true) - .secure(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(); - - let access_token_uuid = uuid::Uuid::parse_str(&access_token.token_uuid.to_string()).unwrap(); - - HttpResponse::Ok() - .cookie(access_cookie) - .cookie(refresh_cookie) - .cookie(logged_in_cookie) - .json(JwtAuth { token: access_token_uuid, user: query_user.into() }) - }, - Err(err) => ResponseError::error_response(&ServiceError { + Err(_) => return ResponseError::error_response(&ServiceError { status: 401, - message: err.to_string() + message: "The email or password was incorrect.".to_string() + }) + }; + // Verify password + if verify_hash(&login_request.password, &query_user.hash) { + let mut refresh_token = RefreshToken::new(&email, &ip_address); + let access_token = match refresh_token.create_access_token() { + Ok(token) => token, + Err(err) => { + error!("Failed to generate access token: {}", 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 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.id.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::from(err)) + }; + + let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token.id.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::from(err)) + }; + + 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.id.clone()) + .path("/") + .max_age(Duration::new(refresh_token_max_age * 60, 0)) + .http_only(true) + .secure(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(refresh_cookie) + .cookie(logged_in_cookie) + .json(JwtAuth { id: access_token.id, user: query_user.into() }) + } else { + return ResponseError::error_response(&ServiceError { + status: 401, + message: "The email or password was incorrect.".to_string() }) } } @@ -139,7 +133,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse { }) }; - let refresh_token_hash = match req.cookie("refresh_token") { + let refresh_token_string = match req.cookie("refresh_token") { Some(cookie) => cookie.value().to_string(), None => return ResponseError::error_response(&ServiceError { status: 401, @@ -155,23 +149,26 @@ async fn refresh(req: HttpRequest) -> HttpResponse { } }; - let refresh_token: RefreshToken = match conn.get::<_, String>(refresh_token_hash.clone()).await { - Ok(result) => match serde_json::from_str(&result) { - Ok(result) => result, + let mut refresh_token: RefreshToken = match conn.get::<_, String>(refresh_token_string.clone()).await { + Ok(result) => match serde_json::from_str::(&result) { + Ok(result) => { + if verify_hash(&ip_address, &result.ip_address) { + result + } else { + return ResponseError::error_response(&ServiceError { + status: 401, + message: "Refresh token is invalid".to_string() + }) + } + }, Err(err) => { error!("Failed to deserialize refresh token: {}", err); - return ResponseError::error_response(&ServiceError { - status: 500, - message: format!("Failed to deserialize refresh token: {}", err) - }) + return ResponseError::error_response(&ServiceError::from(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) - }) + return ResponseError::error_response(&ServiceError::from(err)) } }; @@ -179,46 +176,33 @@ async fn refresh(req: HttpRequest) -> HttpResponse { match QueryUser::get_by_email(&email) { Ok(query_user) => { - let access_token_details = match AccessToken::new(&email) { - Ok(token_details) => token_details, + // Revoke all old access tokens + for id in refresh_token.tokens { + let _: redis::RedisResult<()> = conn.del(id).await; + } + refresh_token.tokens = vec![]; + + // Create new access token + let access_token = match refresh_token.create_access_token() { + Ok(token) => token, Err(err) => { error!("Failed to generate access token: {}", 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 AccessToken::decode(&access_token, &public_key) { - Ok(token_details) => { - let _: redis::RedisResult<()> = conn.del(token_details.token_uuid.to_string()).await; - }, - Err(_) => {} - }; - }, - None => {} - }; 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(), &serde_json::to_string(&access_token_details).unwrap(), (access_token_max_age * 60) as usize).await; + let access_result: redis::RedisResult<()> = conn.set_ex(access_token.id.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 { - status: 500, - message: format!("Failed to set access token in redis: {}", err) - }) + return ResponseError::error_response(&ServiceError::from(err)) }; - let access_cookie = Cookie::build("access_token", access_token_details.token.clone().unwrap()) + let access_cookie = Cookie::build("access_token", access_token.id.clone()) .path("/") .max_age(Duration::new(access_token_max_age * 60, 0)) .http_only(true) @@ -229,8 +213,6 @@ async fn refresh(req: HttpRequest) -> HttpResponse { .max_age(Duration::new(access_token_max_age * 60, 0)) .http_only(false) .finish(); - - let access_token_uuid = uuid::Uuid::parse_str(&access_token_details.token_uuid.to_string()).unwrap(); // Refresh the refresh token if requested let refresh_token_rotation = match params.refresh_token_rotation { @@ -238,8 +220,8 @@ async fn refresh(req: HttpRequest) -> HttpResponse { None => false }; if refresh_token_rotation { - // Delete the old refresh token - let _: redis::RedisResult<()> = conn.del(refresh_token.hash.to_string()).await; + // Delete the old refresh token from redis + let _: redis::RedisResult<()> = conn.del(refresh_token.id.to_string()).await; let refresh_token = RefreshToken::new(&refresh_token.email, &ip_address); let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE") @@ -247,7 +229,8 @@ async fn refresh(req: HttpRequest) -> HttpResponse { .parse::() .expect("REFRESH_TOKEN_MAXAGE must be an integer"); - 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; + // Add the new refresh token to redis + let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token.id.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 { @@ -256,7 +239,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse { }) }; - let refresh_cookie = Cookie::build("refresh_token", refresh_token.hash.clone()) + let refresh_cookie = Cookie::build("refresh_token", refresh_token.id.clone()) .path("/") .max_age(Duration::new(refresh_token_max_age * 60, 0)) .http_only(true) @@ -267,12 +250,12 @@ async fn refresh(req: HttpRequest) -> HttpResponse { .cookie(refresh_cookie) .cookie(access_cookie) .cookie(logged_in_cookie) - .json(JwtAuth { token: access_token_uuid, user: query_user.into() }) + .json(JwtAuth { id: access_token.id, user: query_user.into() }) } else { HttpResponse::Ok() .cookie(access_cookie) .cookie(logged_in_cookie) - .json(JwtAuth { token: access_token_uuid, user: query_user.into() }) + .json(JwtAuth { id: access_token.id, user: query_user.into() }) } }, Err(err) => return ResponseError::error_response(&err) @@ -299,7 +282,7 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { let access_result: redis::RedisResult<()> = conn.del(&[ refresh_token.to_string(), - auth.token.to_string() + auth.id.to_string() ]).await; if let Err(err) = access_result { error!("Failed to set access token in redis: {}", err); @@ -312,11 +295,13 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { let access_cookie = Cookie::build("access_token", "") .path("/") .max_age(Duration::new(-1, 0)) + .secure(true) .http_only(true) .finish(); let refresh_cookie = Cookie::build("refresh_token", "") .path("/") .max_age(Duration::new(-1, 0)) + .secure(true) .http_only(true) .finish(); let logged_in_cookie = Cookie::build("logged_in", "") diff --git a/service/src/auth/tokens.rs b/service/src/auth/tokens.rs index 576c927..0539572 100644 --- a/service/src/auth/tokens.rs +++ b/service/src/auth/tokens.rs @@ -1,15 +1,16 @@ use std::env; -use argon2::password_hash::{rand_core::OsRng, SaltString}; +use argon2::{password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm}; +use rand::prelude::*; +use rand_chacha::ChaCha20Rng; 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 + id: String, // Access Token ID iss: String, // Issuer (Service) exp: i64, // Expiration time iat: i64, // Issued At @@ -18,32 +19,32 @@ struct TokenClaims { #[derive(Debug, Serialize, Deserialize)] pub struct AccessToken { - pub token: Option, - pub token_uuid: uuid::Uuid, - pub email: String, - pub expires_in: Option + pub token: Option, // Access Token + pub id: String, // Access Token ID + pub email: String, // Subject (User) + pub expiration: Option // Expiration time } impl AccessToken { - pub fn new(email: &str) -> Result { + 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 private_key = std::fs::read_to_string(format!("{}/private_key.pem", keys_dir))?; let now = chrono::Utc::now(); let mut token_details = Self { token: None, - token_uuid: uuid::Uuid::new_v4(), + id: csprng_128bit(), email: email.to_string(), - expires_in: Some((now + chrono::Duration::minutes(ttl)).timestamp()) + expiration: 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(), + id: token_details.id.to_string(), + exp: token_details.expiration.unwrap(), iat: now.timestamp(), nbf: now.timestamp() }; @@ -59,29 +60,64 @@ impl AccessToken { 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 }) + let id = decoded.claims.id; + Ok(Self { token: None, id, email, expiration: None }) } } #[derive(Debug, Serialize, Deserialize)] pub struct RefreshToken { - pub hash: String, + pub id: String, pub email: String, pub ip_address: String, - pub timestamp: i64, + pub tokens: Vec, + pub expiration: 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()); + let ttl = env::var("REFRESH_TOKEN_MAXAGE") + .expect("REFRESH_TOKEN_MAXAGE must be set") + .parse::() + .expect("REFRESH_TOKEN_MAXAGE must be an integer"); + let now = chrono::Utc::now(); Self { - hash: format!("{:x}", hash), + id: csprng_128bit(), email: email.to_string(), - ip_address: ip_address.to_string(), - timestamp: now + ip_address: hash(&ip_address).unwrap(), + tokens: vec![], + expiration: (now + chrono::Duration::minutes(ttl)).timestamp() } } + + pub fn create_access_token(&mut self) -> Result { + let access_token = AccessToken::new(&self.email)?; + self.tokens.push(access_token.id.clone()); + Ok(access_token) + } +} + +fn csprng_128bit() -> String { + // Generate a CSPRNG 128-bit (16 byte) ID using alphanumeric characters (a-z, A-Z, 0-9) + let rng = ChaCha20Rng::from_entropy(); + rng.sample_iter(rand::distributions::Alphanumeric).take(16).map(char::from).collect() +} + +pub fn hash(str: &str) -> Result { + let salt = SaltString::generate(&mut OsRng); + let bytes = str.as_bytes(); + let hash = Argon2::default().hash_password(bytes, &salt)?.to_string(); + Ok(hash) +} + +pub fn verify_hash(str: &str, hash: &str) -> bool { + let bytes = str.as_bytes(); + let parsed_hash = match PasswordHash::new(hash) { + Ok(h) => h, + Err(_) => return false + }; + match Argon2::default().verify_password(bytes, &parsed_hash) { + Ok(_) => true, + Err(_) => false + } } \ No newline at end of file