From cf1107d09c9cec9b9ba117734f1ccf9fd5d8490e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20Kaan=20G=C3=9CM=C3=9C=C5=9E?= <96421894+Tahinli@users.noreply.github.com> Date: Tue, 14 Jan 2025 00:59:21 +0300 Subject: [PATCH] feat: :sparkles: refresh token system --- .env | 6 +- .gitignore | 23 -------- configs/server_config.toml | 5 +- migrations/20250110224901_login.up.sql | 7 ++- src/database/login.rs | 22 +------- src/feature/login.rs | 77 ++++++++++++++++++++++---- src/lib.rs | 8 ++- 7 files changed, 85 insertions(+), 63 deletions(-) diff --git a/.env b/.env index 4fa7ffb..a46ffb9 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ -# This is for sqlx to do compile time sql checking -# Actual server and database configs are in config folder -DATABASE_URL=postgres://root:root@localhost:5432/rust_forum \ No newline at end of file +# This is for sqlx to do compile time SQL checking. +# Actual server and database configs are in "configs" folder which is in main directory by default. +DATABASE_URL=postgres://root:root@localhost:5432/rust_forum diff --git a/.gitignore b/.gitignore index 04a5683..1041a51 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,4 @@ .vscode/ - -# Generated by Cargo -# will have compiled files and executables debug/ target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb - -# RustRover -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Added by cargo - -/target \ No newline at end of file diff --git a/configs/server_config.toml b/configs/server_config.toml index b7c16ec..a39ad56 100644 --- a/configs/server_config.toml +++ b/configs/server_config.toml @@ -1,5 +1,6 @@ [server_config] address = "localhost:2344" otp_time_limit = 15 -login_token_time_limit = 15 -concurrency_limit = -1 \ No newline at end of file +login_token_expiration_time_limit = 15 +login_token_refresh_time_limit = 30 +concurrency_limit = -1 diff --git a/migrations/20250110224901_login.up.sql b/migrations/20250110224901_login.up.sql index f0fa763..e889a5b 100644 --- a/migrations/20250110224901_login.up.sql +++ b/migrations/20250110224901_login.up.sql @@ -1,6 +1,7 @@ -- Add up migration script here -CREATE TABLE IF NOT EXISTS "login"( - user_id BIGSERIAL NOT NULL REFERENCES "user"(id), +CREATE TABLE IF NOT EXISTS "login" ( + user_id BIGSERIAL NOT NULL REFERENCES "user" (id), token VARCHAR(1024) NOT NULL, + token_creation_time TIMESTAMPTZ NOT NULL DEFAULT NOW (), PRIMARY KEY (user_id, token) -); \ No newline at end of file +); diff --git a/src/database/login.rs b/src/database/login.rs index 9577ab0..ca54cfb 100644 --- a/src/database/login.rs +++ b/src/database/login.rs @@ -10,8 +10,8 @@ pub async fn create( sqlx::query_as!( Login, r#" - INSERT INTO "login"(user_id, token) - VALUES ($1, $2) + INSERT INTO "login"(user_id, token) + VALUES ($1, $2) RETURNING * "#, user_id, @@ -38,24 +38,6 @@ pub async fn read( .await } -pub async fn update( - user_id: &i64, - token: &String, - database_connection: &Pool, -) -> Result { - sqlx::query_as!( - Login, - r#" - UPDATE "login" SET "token" = $2 WHERE "user_id" = $1 - RETURNING * - "#, - user_id, - token, - ) - .fetch_one(database_connection) - .await -} - pub async fn delete( user_id: &i64, token: &String, diff --git a/src/feature/login.rs b/src/feature/login.rs index d6a750a..faa2ddf 100644 --- a/src/feature/login.rs +++ b/src/feature/login.rs @@ -1,3 +1,6 @@ +use std::sync::LazyLock; + +use chrono::{DateTime, Utc}; use jwt_simple::{ claims::Claims, common::VerificationOptions, @@ -8,10 +11,54 @@ use sqlx::{Pool, Postgres}; use crate::{database::login, SERVER_CONFIG}; +static TOKEN_META: LazyLock = LazyLock::new(TokenMeta::init); + +struct TokenMeta { + token_key: HS256Key, + token_verification_options: Option, +} + +impl TokenMeta { + fn init() -> Self { + Self { + token_key: HS256Key::generate(), + token_verification_options: { + let mut verification_options = VerificationOptions::default(); + verification_options.time_tolerance = Some(jwt_simple::prelude::Duration::from(0)); + Some(verification_options) + }, + } + } + + async fn create_token() -> Option { + let key = &TOKEN_META.token_key; + let claims = Claims::create(jwt_simple::prelude::Duration::from_mins( + SERVER_CONFIG.login_token_expiration_time_limit as u64, + )); + let token = key.authenticate(claims).unwrap(); + match TokenMeta::verify_token(&token).await { + true => Some(token), + false => None, + } + } + + async fn verify_token(token: &String) -> bool { + let token_meta = &TOKEN_META; + token_meta + .token_key + .verify_token::( + token, + token_meta.token_verification_options.clone(), + ) + .is_ok() + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct Login { pub user_id: i64, pub token: String, + pub token_creation_time: DateTime, } impl Login { @@ -19,15 +66,9 @@ impl Login { user_id: &i64, database_connection: &Pool, ) -> Result { - let key = HS256Key::generate(); - let claims = Claims::create(jwt_simple::prelude::Duration::from_mins( - SERVER_CONFIG.login_token_time_limit as u64, - )); - let mut verification_options = VerificationOptions::default(); - verification_options.time_tolerance = Some(jwt_simple::prelude::Duration::from(0)); - - let token = key.authenticate(claims).unwrap(); - + let token = TokenMeta::create_token() + .await + .expect("Should not panic if it isn't configured wrong"); login::create(user_id, &token, database_connection).await } @@ -44,7 +85,23 @@ impl Login { token: &String, database_connection: &Pool, ) -> Result { - login::update(user_id, token, database_connection).await + let login = Login::read(user_id, token, database_connection).await?; + + match TokenMeta::verify_token(token).await { + true => Ok(login), + false => { + if DateTime::::default() + .signed_duration_since(&login.token_creation_time) + .num_minutes() + <= SERVER_CONFIG.login_token_refresh_time_limit as i64 + { + Login::delete(user_id, token, database_connection).await?; + Login::create(user_id, database_connection).await + } else { + Ok(login) + } + } + } } pub async fn delete( user_id: &i64, diff --git a/src/lib.rs b/src/lib.rs index f5d5727..1148404 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,8 @@ impl Default for DatabaseConfig { pub struct ServerConfig { pub address: String, pub otp_time_limit: usize, - pub login_token_time_limit: usize, + pub login_token_expiration_time_limit: usize, + pub login_token_refresh_time_limit: usize, pub concurrency_limit: usize, } @@ -65,7 +66,10 @@ impl Default for ServerConfig { Self { address: server_configs.pop_front().unwrap().parse().unwrap(), otp_time_limit: value_or_max(server_configs.pop_front().unwrap()), - login_token_time_limit: value_or_max(server_configs.pop_front().unwrap()), + login_token_expiration_time_limit: value_or_max( + server_configs.pop_front().unwrap(), + ), + login_token_refresh_time_limit: value_or_max(server_configs.pop_front().unwrap()), concurrency_limit: value_or_max(server_configs.pop_front().unwrap()), } } else {