From 9dd3811136f361284c5e6a9ee5618a2459e3865e Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Fri, 16 Jun 2023 18:40:21 +0200 Subject: [PATCH] Refactor login tokens management --- geneit_backend/src/constants.rs | 9 ++ .../src/services/login_token_service.rs | 140 ++++++++++++------ geneit_backend/src/services/users_service.rs | 4 +- 3 files changed, 104 insertions(+), 49 deletions(-) diff --git a/geneit_backend/src/constants.rs b/geneit_backend/src/constants.rs index ad5473d..82b736f 100644 --- a/geneit_backend/src/constants.rs +++ b/geneit_backend/src/constants.rs @@ -45,5 +45,14 @@ pub const ACCOUNT_DELETE_TOKEN_DURATION: Duration = Duration::from_secs(3600 * 1 /// OpenID state duration pub const OPEN_ID_STATE_DURATION: Duration = Duration::from_secs(3600); +/// Auth token duration +pub const AUTH_TOKEN_DURATION: Duration = Duration::from_secs(3600 * 24); + +/// Minimum interval before heartbeat update +pub const AUTH_TOKEN_HB_MIN_INTERVAL: Duration = Duration::from_secs(60); + +/// Auth token max inactivity period +pub const AUTH_TOKEN_MAX_INACTIVITY: Duration = Duration::from_secs(3600); + /// Length of family invitation code pub const FAMILY_INVITATION_CODE_LEN: usize = 7; diff --git a/geneit_backend/src/services/login_token_service.rs b/geneit_backend/src/services/login_token_service.rs index 89ba882..8b9e699 100644 --- a/geneit_backend/src/services/login_token_service.rs +++ b/geneit_backend/src/services/login_token_service.rs @@ -1,90 +1,133 @@ //! # User tokens management use crate::connections::redis_connection; +use crate::constants::{ + AUTH_TOKEN_DURATION, AUTH_TOKEN_HB_MIN_INTERVAL, AUTH_TOKEN_MAX_INACTIVITY, +}; use crate::models::{User, UserID}; -use crate::utils::string_utils; +use crate::utils::string_utils::rand_str; use crate::utils::time_utils::time; use actix_web::dev::Payload; use actix_web::{FromRequest, HttpRequest}; use std::future::{ready, Ready}; -use std::time::Duration; + +#[derive(thiserror::Error, Debug)] +enum LoginTokenServiceError { + #[error("Malformed login token!")] + MalformedToken, +} #[derive(Debug, Clone)] pub struct LoginTokenValue(String); +impl LoginTokenValue { + pub fn parse(&self) -> anyhow::Result<(UserID, &str)> { + let (id, key) = self + .0 + .split_once('-') + .ok_or(LoginTokenServiceError::MalformedToken)?; + + Ok((UserID(id.parse()?), key)) + } +} + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct LoginToken { #[serde(rename = "e")] expire: u64, #[serde(rename = "h")] hb: u64, + #[serde(rename = "t")] + key: String, #[serde(rename = "u")] pub user_id: UserID, } impl LoginToken { - pub fn new(user: &User) -> (String, Self) { - let key = format!("{}-{}", user.id().0, string_utils::rand_str(40)); - - ( - key, - Self { - expire: time() + 3600 * 24, - hb: time(), - user_id: user.id(), - }, - ) - } - - pub fn is_expired(&self) -> bool { - self.expire < time() || self.hb + 3600 < time() - } - - pub fn refresh_hb(&self) -> Option { - if self.hb + 60 * 5 < time() { - let mut new = self.clone(); - new.hb = time(); - Some(new) - } else { - None + fn new(user_id: UserID) -> Self { + Self { + expire: time() + AUTH_TOKEN_DURATION.as_secs(), + hb: time(), + key: rand_str(40), + user_id, } } - pub fn redis_lifetime(&self) -> Duration { - Duration::from_secs(if self.expire <= time() { - 0 - } else { - self.expire - time() - }) + fn is_expired(&self) -> bool { + self.expire < time() || self.hb + AUTH_TOKEN_MAX_INACTIVITY.as_secs() < time() + } + + fn refresh_hb(&mut self) -> bool { + if self.hb + AUTH_TOKEN_HB_MIN_INTERVAL.as_secs() < time() { + self.hb = time(); + return true; + } + false + } + + /// Get the token returned to the user + fn key(&self) -> String { + format!("{}-{}", self.user_id.0, self.key) } } -/// Get redis key for a given login token -fn redis_key(tok: &str) -> String { - format!("tok-{tok}") +/// Get redis key for a given user +fn redis_key(user_id: &UserID) -> String { + format!("tok-{}", user_id.0) +} + +/// Get the tokens of a user +async fn get_user_tokens(user_id: UserID) -> anyhow::Result> { + Ok(redis_connection::get_value(&redis_key(&user_id)) + .await? + .unwrap_or_default()) +} + +async fn set_user_tokens(user_id: UserID, mut tokens: Vec) -> anyhow::Result<()> { + tokens.retain(|t| !t.is_expired()); + + redis_connection::set_value(&redis_key(&user_id), &tokens, AUTH_TOKEN_MAX_INACTIVITY).await } /// Generate a new login token pub async fn gen_new_token(user: &User) -> anyhow::Result { - let (key, token) = LoginToken::new(user); - redis_connection::set_value(&redis_key(&key), &token, token.redis_lifetime()).await?; + let mut tokens = get_user_tokens(user.id()).await?; + + let token = LoginToken::new(user.id()); + let key = token.key(); + + tokens.push(token); + set_user_tokens(user.id(), tokens).await?; + Ok(key) } -/// Delete a login token +/// Delete a login token (disconnect a client) pub async fn delete_token(token: &LoginTokenValue) -> anyhow::Result<()> { - redis_connection::remove_value(&redis_key(&token.0)).await?; - Ok(()) + let (user_id, key) = token.parse()?; + let mut tokens = get_user_tokens(user_id).await?; + + tokens.retain(|t| t.key != key); + + set_user_tokens(user_id, tokens).await +} + +/// Remove all the tokens of a user (disconnect it from all devices) +pub async fn disconnect_user_from_all_devices(user_id: UserID) -> anyhow::Result<()> { + set_user_tokens(user_id, vec![]).await } /// Get a user information from its token -async fn get_token(key: &str) -> anyhow::Result> { - let token = match redis_connection::get_value::(&redis_key(key)).await? { +async fn load_token_info(token: &LoginTokenValue) -> anyhow::Result> { + let (user_id, key) = token.parse()?; + let mut user_tokens = get_user_tokens(user_id).await?; + + let token = match user_tokens.iter_mut().find(|t| t.key == key) { + Some(t) => t, None => { - log::error!("Could not find token for key '{}' (key absent)", key); + log::error!("Could not find token for key '{}' (missing token)", key); return Ok(None); } - Some(t) => t, }; if token.is_expired() { @@ -93,11 +136,12 @@ async fn get_token(key: &str) -> anyhow::Result> { } // Check if heartbeat must be updated - if let Some(renew) = token.refresh_hb() { - redis_connection::set_value(&redis_key(key), &renew, renew.redis_lifetime()).await?; + let clone = token.clone(); + if token.refresh_hb() { + set_user_tokens(clone.user_id, user_tokens).await?; } - Ok(Some(token)) + Ok(Some(clone)) } impl FromRequest for LoginTokenValue { @@ -123,7 +167,7 @@ impl FromRequest for LoginToken { Box::pin(async move { let token = LoginTokenValue::extract(&req).await?; - let token = match get_token(&token.0).await { + let token = match load_token_info(&token).await { Err(e) => { log::error!("Failed to load auth token! {}", e); return Err(actix_web::error::ErrorInternalServerError( diff --git a/geneit_backend/src/services/users_service.rs b/geneit_backend/src/services/users_service.rs index 93f2c9f..b60db33 100644 --- a/geneit_backend/src/services/users_service.rs +++ b/geneit_backend/src/services/users_service.rs @@ -5,7 +5,7 @@ use crate::connections::db_connection; use crate::constants::{ACCOUNT_DELETE_TOKEN_DURATION, PASSWORD_RESET_TOKEN_DURATION}; use crate::models::{NewUser, User, UserID}; use crate::schema::users; -use crate::services::mail_service; +use crate::services::{login_token_service, mail_service}; use crate::utils::string_utils::rand_str; use crate::utils::time_utils::time; use bcrypt::DEFAULT_COST; @@ -173,6 +173,8 @@ pub async fn delete_account(user: &User) -> anyhow::Result<()> { // TODO : remove families memberships + login_token_service::disconnect_user_from_all_devices(user.id()).await?; + db_connection::execute(|conn| { diesel::delete(users::dsl::users.filter(users::dsl::id.eq(user.id))).execute(conn)?; Ok(())