From 798c535c193d81a67128a8092883a223edae7430 Mon Sep 17 00:00:00 2001 From: EleisonC Date: Sun, 16 Jun 2024 21:45:23 +0300 Subject: [PATCH 1/2] [sprint6][task5]ft-implment redis for banned token --- auth-service/Cargo.lock | 42 ++++++++++++++ auth-service/Cargo.toml | 1 + auth-service/Dockerfile | 1 + auth-service/src/lib.rs | 6 ++ auth-service/src/main.rs | 12 +++- auth-service/src/services/data_stores/mod.rs | 4 +- .../data_stores/redis_banned_token_store.rs | 55 +++++++++++++++++++ auth-service/src/utils/constants.rs | 8 +++ auth-service/tests/api/helpers.rs | 14 ++++- compose.yml | 10 +++- 10 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 auth-service/src/services/data_stores/redis_banned_token_store.rs diff --git a/auth-service/Cargo.lock b/auth-service/Cargo.lock index d919ce4..df5c187 100644 --- a/auth-service/Cargo.lock +++ b/auth-service/Cargo.lock @@ -108,6 +108,7 @@ dependencies = [ "quickcheck", "quickcheck_macros", "rand 0.8.5", + "redis", "reqwest", "serde", "serde_json", @@ -313,6 +314,20 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1609,6 +1624,27 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "redis" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d7a6955c7511f60f3ba9e86c6d02b3c3f144f8c24b288d1f4e18074ab8bbec" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1870,6 +1906,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.10.8" diff --git a/auth-service/Cargo.toml b/auth-service/Cargo.toml index d30dba7..4fd33ac 100644 --- a/auth-service/Cargo.toml +++ b/auth-service/Cargo.toml @@ -23,6 +23,7 @@ url = "2" rand = "0.8.5" sqlx = { version = "0.7", features = [ "runtime-tokio-rustls", "postgres", "migrate"] } argon2 = { version = "0.5.3", features = ["std"] } +redis = { version = "0.25.2", features = ["tokio-comp"] } [dev-dependencies] fake = "=2.3.0" diff --git a/auth-service/Dockerfile b/auth-service/Dockerfile index bbf0eb1..7e75aa7 100644 --- a/auth-service/Dockerfile +++ b/auth-service/Dockerfile @@ -25,4 +25,5 @@ FROM debian:buster-slim AS runtime WORKDIR /app COPY --from=builder /app/target/release/auth-service /usr/local/bin COPY --from=builder /app/assets /app/assets +ENV REDIS_HOST_NAME=redis ENTRYPOINT ["/usr/local/bin/auth-service"] \ No newline at end of file diff --git a/auth-service/src/lib.rs b/auth-service/src/lib.rs index 5962967..aa5e076 100644 --- a/auth-service/src/lib.rs +++ b/auth-service/src/lib.rs @@ -6,6 +6,7 @@ use axum::{ http::{Method, StatusCode}, Json }; +use redis::{Client, RedisResult}; use sqlx::{postgres::PgPoolOptions, PgPool}; use tower_http::{services::ServeDir, cors::CorsLayer}; use app_state::AppState; @@ -97,3 +98,8 @@ impl Application { pub async fn get_postgres_pool(url: &str) -> Result { PgPoolOptions::new().max_connections(5).connect(url).await } + +pub fn get_redis_client(redis_hostname: String) -> RedisResult { + let redis_url = format!("redis://{}/", redis_hostname); + redis::Client::open(redis_url) +} \ No newline at end of file diff --git a/auth-service/src/main.rs b/auth-service/src/main.rs index b7862f2..1923237 100644 --- a/auth-service/src/main.rs +++ b/auth-service/src/main.rs @@ -2,14 +2,15 @@ use std::sync::Arc; use sqlx::PgPool; use tokio::sync::RwLock; -use auth_service::{app_state::AppState, get_postgres_pool, services, utils::constants::{prod, DATABASE_URL}, Application}; +use auth_service::{app_state::AppState, get_postgres_pool, get_redis_client, services, utils::constants::{prod, DATABASE_URL, REDIS_HOST_NAME}, Application}; #[tokio::main] async fn main() { let pg_pool = configure_postgresql().await; + let redis_client = Arc::new(RwLock::new(configure_redis())); let user_store = Arc::new(RwLock::new(services::PostgresUserStore::new(pg_pool))); - let banned_token_store = Arc::new(RwLock::new(services::HashsetBannedTokenStore::default())); + let banned_token_store = Arc::new(RwLock::new(services::RedisBannedTokenStore::new(redis_client))); let two_fa_code_store = Arc::new(RwLock::new(services::HashmapTwoFACodeStore::default())); let email_client = Arc::new(RwLock::new(services::MockEmailClient::default())); @@ -33,3 +34,10 @@ async fn configure_postgresql() -> PgPool { .expect("Failed to run migrations"); pg_pool } + +fn configure_redis() -> redis::Connection { + get_redis_client(REDIS_HOST_NAME.to_owned()) + .expect("Failed to get Redis client") + .get_connection() + .expect("Failed to get Redis client") +} diff --git a/auth-service/src/services/data_stores/mod.rs b/auth-service/src/services/data_stores/mod.rs index 41d0a5f..f41b95a 100644 --- a/auth-service/src/services/data_stores/mod.rs +++ b/auth-service/src/services/data_stores/mod.rs @@ -3,10 +3,12 @@ pub mod hashset_banned_token; pub mod hashmap_two_fa_code_store; pub mod mock_email_client; pub mod postgres_user_store; +pub mod redis_banned_token_store; pub use hashmap_user_store::*; pub use hashset_banned_token::*; pub use hashmap_two_fa_code_store::*; pub use mock_email_client::*; -pub use postgres_user_store::*; \ No newline at end of file +pub use postgres_user_store::*; +pub use redis_banned_token_store::*; \ No newline at end of file diff --git a/auth-service/src/services/data_stores/redis_banned_token_store.rs b/auth-service/src/services/data_stores/redis_banned_token_store.rs new file mode 100644 index 0000000..94e9ae9 --- /dev/null +++ b/auth-service/src/services/data_stores/redis_banned_token_store.rs @@ -0,0 +1,55 @@ +use std::sync::Arc; + +use redis::{Commands, Connection}; +use tokio::sync::RwLock; + +use crate::{ + domain::data_stores::{ + BannedTokenStore, BannedTokenStoreError}, + utils::auth::TOKEN_TTL_SECONDS, +}; + +pub struct RedisBannedTokenStore { + conn: Arc> +} + +impl RedisBannedTokenStore { + pub fn new(conn: Arc>) -> Self { + Self{ conn } + } +} + +#[async_trait::async_trait] +impl BannedTokenStore for RedisBannedTokenStore { + async fn store_banned_token(&mut self, token: String) -> Result<(), BannedTokenStoreError> { + let redis_token_key = get_key(&token); + let mut store_conn = self.conn.write().await; + + let ttl = TOKEN_TTL_SECONDS as u64; + let result = store_conn.set_ex::(redis_token_key, true, ttl); + + match result { + Ok(_) => Ok(()), + Err(_) => Err(BannedTokenStoreError::UnexpectedError) + } + } + + async fn check_banned_token(&self, token: String) -> Result { + let redis_token_key = get_key(&token); + let mut store_conn = self.conn.write().await; + + let result = store_conn.exists(redis_token_key); + + match result { + Ok(true) => Ok(format!("Token {} is banned", token)), + Ok(false) => Err(BannedTokenStoreError::TokenNotFound), + Err(_) => Err(BannedTokenStoreError::UnexpectedError) + } + } +} + +const BANNED_TOKEN_KEY_PREFIX: &str = "banned_token:"; + +fn get_key(token: &str) -> String { + return format!("{}{}", BANNED_TOKEN_KEY_PREFIX, token); +} diff --git a/auth-service/src/utils/constants.rs b/auth-service/src/utils/constants.rs index 6a863cc..bfcdf49 100644 --- a/auth-service/src/utils/constants.rs +++ b/auth-service/src/utils/constants.rs @@ -5,6 +5,7 @@ use std::env as std_env; lazy_static! { pub static ref JWT_SECRET: String = set_token(); pub static ref DATABASE_URL: String = set_database_url(); + pub static ref REDIS_HOST_NAME: String = set_redis_host(); } fn set_token() -> String { @@ -31,12 +32,19 @@ fn set_database_url() -> String { database_url } +fn set_redis_host() -> String { + dotenv().ok(); + std_env::var(env::REDIS_HOST_NAME_ENV_VAR) + .unwrap_or(DEFAULT_REDIS_HOSTNAME.to_owned()) +} pub mod env { pub const JWT_SECRET_ENV_VAR: &str = "JWT_SECRET"; pub const DATABASE_URL_ENV_VAR: &str = "DATABASE_URL"; + pub const REDIS_HOST_NAME_ENV_VAR: &str = "REDIS_HOST_NAME"; } pub const JWT_COOKIE_NAME: &str = "jwt"; +pub const DEFAULT_REDIS_HOSTNAME: &str = "127.0.0.1"; pub mod prod { pub const APP_ADDRESS: &str = "0.0.0.0:3000"; diff --git a/auth-service/tests/api/helpers.rs b/auth-service/tests/api/helpers.rs index bb3a691..767bf69 100644 --- a/auth-service/tests/api/helpers.rs +++ b/auth-service/tests/api/helpers.rs @@ -1,4 +1,4 @@ -use auth_service::{app_state::AppState, get_postgres_pool, services::{self, HashmapTwoFACodeStore}, utils::constants::{test, DATABASE_URL}, Application}; +use auth_service::{app_state::AppState, get_postgres_pool, get_redis_client, services::{self, HashmapTwoFACodeStore}, utils::constants::{test, DATABASE_URL, DEFAULT_REDIS_HOSTNAME}, Application}; use sqlx::{postgres::{PgConnectOptions, PgPoolOptions}, Connection, Executor, PgConnection, PgPool}; use uuid::Uuid; use std::{str::FromStr, sync::Arc}; @@ -19,9 +19,10 @@ impl TestApp { pub async fn new() -> Self { let db_name = Uuid::new_v4().to_string(); let pg_pool = configure_postgresql(&db_name).await; + let redis_client = Arc::new(RwLock::new(configure_redis())); let test_user_store = Arc::new(RwLock::new(services::PostgresUserStore::new(pg_pool))); - let test_banned_token_store = Arc::new(RwLock::new(services::HashsetBannedTokenStore::default())); + let test_banned_token_store = Arc::new(RwLock::new(services::RedisBannedTokenStore::new(redis_client))); let two_fa_code_store = Arc::new(RwLock::new(services::HashmapTwoFACodeStore::default())); let email_client = Arc::new(RwLock::new(services::MockEmailClient::default())); @@ -205,3 +206,12 @@ async fn delete_database(db_name: &str) { .await .expect("Failed to drop the database."); } + +fn configure_redis() -> redis::Connection { + let redis_hostname = DEFAULT_REDIS_HOSTNAME.to_owned(); + + get_redis_client(redis_hostname) + .expect("Failed to get Redis client") + .get_connection() + .expect("Failed to get Redis connection") +} diff --git a/compose.yml b/compose.yml index d25e7dd..406852e 100644 --- a/compose.yml +++ b/compose.yml @@ -1,9 +1,9 @@ version: "3.9" services: app-service: - # TODO: change "letsgetrusty" to your Docker Hub username image: redwallet212/app-service # specify name of image on Docker Hub restart: "always" # automatically restart container when server crashes + container_name: app-service environment: # set up environment variables AUTH_SERVICE_IP: ${AUTH_SERVICE_IP} ports: @@ -12,7 +12,6 @@ services: auth-service: condition: service_started auth-service: - # TODO: change "letsgetrusty" to your Docker Hub username image: redwallet212/auth-service restart: always # automatically restart container when server crashes container_name: auth-service @@ -23,6 +22,7 @@ services: - "3000:3000" # expose port 3000 so that applications outside the container can connect to it depends_on: - db + - redis db: image: postgres:15.2-alpine restart: always @@ -33,6 +33,12 @@ services: - "5432:5432" volumes: - db_data:/var/lib/postgresql/data + redis: + image: redis:7.0-alpine + restart: always + container_name: redis + ports: + - "6379:6379" volumes: db_data: From c9cc0416c7870fdec6068599dca16a36ed379d8e Mon Sep 17 00:00:00 2001 From: EleisonC Date: Sun, 16 Jun 2024 22:42:34 +0300 Subject: [PATCH 2/2] [sprint6][task6]ft-implement redis for two fa code store --- auth-service/src/main.rs | 4 +- auth-service/src/services/data_stores/mod.rs | 4 +- .../data_stores/redis_two_fa_code_store.rs | 81 +++++++++++++++++++ auth-service/tests/api/helpers.rs | 8 +- 4 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 auth-service/src/services/data_stores/redis_two_fa_code_store.rs diff --git a/auth-service/src/main.rs b/auth-service/src/main.rs index 1923237..59688ec 100644 --- a/auth-service/src/main.rs +++ b/auth-service/src/main.rs @@ -10,8 +10,8 @@ async fn main() { let redis_client = Arc::new(RwLock::new(configure_redis())); let user_store = Arc::new(RwLock::new(services::PostgresUserStore::new(pg_pool))); - let banned_token_store = Arc::new(RwLock::new(services::RedisBannedTokenStore::new(redis_client))); - let two_fa_code_store = Arc::new(RwLock::new(services::HashmapTwoFACodeStore::default())); + let banned_token_store = Arc::new(RwLock::new(services::RedisBannedTokenStore::new(redis_client.clone()))); + let two_fa_code_store = Arc::new(RwLock::new(services::RedisTwoFACodeStore::new(redis_client))); let email_client = Arc::new(RwLock::new(services::MockEmailClient::default())); let app_state = AppState::new( diff --git a/auth-service/src/services/data_stores/mod.rs b/auth-service/src/services/data_stores/mod.rs index f41b95a..3ba6161 100644 --- a/auth-service/src/services/data_stores/mod.rs +++ b/auth-service/src/services/data_stores/mod.rs @@ -4,6 +4,7 @@ pub mod hashmap_two_fa_code_store; pub mod mock_email_client; pub mod postgres_user_store; pub mod redis_banned_token_store; +pub mod redis_two_fa_code_store; pub use hashmap_user_store::*; @@ -11,4 +12,5 @@ pub use hashset_banned_token::*; pub use hashmap_two_fa_code_store::*; pub use mock_email_client::*; pub use postgres_user_store::*; -pub use redis_banned_token_store::*; \ No newline at end of file +pub use redis_banned_token_store::*; +pub use redis_two_fa_code_store::*; \ No newline at end of file diff --git a/auth-service/src/services/data_stores/redis_two_fa_code_store.rs b/auth-service/src/services/data_stores/redis_two_fa_code_store.rs new file mode 100644 index 0000000..0ef0982 --- /dev/null +++ b/auth-service/src/services/data_stores/redis_two_fa_code_store.rs @@ -0,0 +1,81 @@ +use std::sync::Arc; + +use redis::{Commands, Connection}; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; + +use crate::domain::{ + data_stores::{ TwoFACodeStore, TwoFACodeStoreError}, + Email, LoginAttemptId, TwoFACode, +}; + +pub struct RedisTwoFACodeStore { + conn: Arc> +} + +impl RedisTwoFACodeStore { + pub fn new(conn: Arc>) -> Self { + Self{ conn } + } +} + +#[async_trait::async_trait] +impl TwoFACodeStore for RedisTwoFACodeStore { + async fn add_code(&mut self, + email: Email, + login_attempt_id: LoginAttemptId, + code: TwoFACode + ) -> Result<(), TwoFACodeStoreError> { + let mut conn = self.conn.write().await; + let key = get_key(&email); + + let two_fa_tuple = TwoFATuple(login_attempt_id.as_ref().to_owned(), code.as_ref().to_owned()); + + let serialized_data = serde_json::to_string(&two_fa_tuple).map_err(|_| TwoFACodeStoreError::UnexpectedError)?; + + let _:() = conn + .set_ex(&key, serialized_data, TEN_MINUTES_IN_SECONDS) + .map_err(|_| TwoFACodeStoreError::UnexpectedError)?; + + Ok(()) + } + + async fn remove_code(&mut self, email: &Email) -> Result<(), TwoFACodeStoreError> { + let mut conn = self.conn.write().await; + let key = get_key(email); + + let _:() = conn + .del(&key) + .map_err(|_| TwoFACodeStoreError::UnexpectedError)?; + + Ok(()) + } + + async fn get_code( + &self, + email: &Email, + ) -> Result<(LoginAttemptId, TwoFACode), TwoFACodeStoreError> { + let mut conn = self.conn.write().await; + let key = get_key(email); + + match conn.get::<_, String>(&key) { + Ok(data) => { + let two_fa_tuple: TwoFATuple = serde_json::from_str(&data) + .map_err(|_| TwoFACodeStoreError::UnexpectedError)?; + + Ok((LoginAttemptId::parse(two_fa_tuple.0).unwrap(), TwoFACode::parse(two_fa_tuple.1).unwrap())) + }, + Err(_) => Err(TwoFACodeStoreError::LoginAttemptIdNotFound) + } + } +} + +#[derive(Serialize, Deserialize)] +struct TwoFATuple(pub String, pub String); + +const TEN_MINUTES_IN_SECONDS: u64 = 600; +const TWO_FA_CODE_PREFIX: &str = "two_fa_code:"; + +fn get_key(email: &Email) -> String { + format!("{}{}", TWO_FA_CODE_PREFIX, email.as_ref()) +} diff --git a/auth-service/tests/api/helpers.rs b/auth-service/tests/api/helpers.rs index 767bf69..5a8b46b 100644 --- a/auth-service/tests/api/helpers.rs +++ b/auth-service/tests/api/helpers.rs @@ -1,4 +1,4 @@ -use auth_service::{app_state::AppState, get_postgres_pool, get_redis_client, services::{self, HashmapTwoFACodeStore}, utils::constants::{test, DATABASE_URL, DEFAULT_REDIS_HOSTNAME}, Application}; +use auth_service::{app_state::AppState, get_postgres_pool, get_redis_client, services::{self, RedisTwoFACodeStore}, utils::constants::{test, DATABASE_URL, DEFAULT_REDIS_HOSTNAME}, Application}; use sqlx::{postgres::{PgConnectOptions, PgPoolOptions}, Connection, Executor, PgConnection, PgPool}; use uuid::Uuid; use std::{str::FromStr, sync::Arc}; @@ -11,7 +11,7 @@ pub struct TestApp { pub http_client: reqwest::Client, pub db_name: String, pub cookie_jar: Arc, - pub two_fa_code_store: Arc>, + pub two_fa_code_store: Arc>, clean_up_called: bool } @@ -22,8 +22,8 @@ impl TestApp { let redis_client = Arc::new(RwLock::new(configure_redis())); let test_user_store = Arc::new(RwLock::new(services::PostgresUserStore::new(pg_pool))); - let test_banned_token_store = Arc::new(RwLock::new(services::RedisBannedTokenStore::new(redis_client))); - let two_fa_code_store = Arc::new(RwLock::new(services::HashmapTwoFACodeStore::default())); + let test_banned_token_store = Arc::new(RwLock::new(services::RedisBannedTokenStore::new(redis_client.clone()))); + let two_fa_code_store = Arc::new(RwLock::new(services::RedisTwoFACodeStore::new(redis_client))); let email_client = Arc::new(RwLock::new(services::MockEmailClient::default())); let test_app_state = AppState::new(test_user_store, test_banned_token_store, two_fa_code_store.clone(), email_client);