Refactor login tokens management
This commit is contained in:
		@@ -45,5 +45,14 @@ pub const ACCOUNT_DELETE_TOKEN_DURATION: Duration = Duration::from_secs(3600 * 1
 | 
				
			|||||||
/// OpenID state duration
 | 
					/// OpenID state duration
 | 
				
			||||||
pub const OPEN_ID_STATE_DURATION: Duration = Duration::from_secs(3600);
 | 
					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
 | 
					/// Length of family invitation code
 | 
				
			||||||
pub const FAMILY_INVITATION_CODE_LEN: usize = 7;
 | 
					pub const FAMILY_INVITATION_CODE_LEN: usize = 7;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,90 +1,133 @@
 | 
				
			|||||||
//! # User tokens management
 | 
					//! # User tokens management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::connections::redis_connection;
 | 
					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::models::{User, UserID};
 | 
				
			||||||
use crate::utils::string_utils;
 | 
					use crate::utils::string_utils::rand_str;
 | 
				
			||||||
use crate::utils::time_utils::time;
 | 
					use crate::utils::time_utils::time;
 | 
				
			||||||
use actix_web::dev::Payload;
 | 
					use actix_web::dev::Payload;
 | 
				
			||||||
use actix_web::{FromRequest, HttpRequest};
 | 
					use actix_web::{FromRequest, HttpRequest};
 | 
				
			||||||
use std::future::{ready, Ready};
 | 
					use std::future::{ready, Ready};
 | 
				
			||||||
use std::time::Duration;
 | 
					
 | 
				
			||||||
 | 
					#[derive(thiserror::Error, Debug)]
 | 
				
			||||||
 | 
					enum LoginTokenServiceError {
 | 
				
			||||||
 | 
					    #[error("Malformed login token!")]
 | 
				
			||||||
 | 
					    MalformedToken,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
pub struct LoginTokenValue(String);
 | 
					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)]
 | 
					#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | 
				
			||||||
pub struct LoginToken {
 | 
					pub struct LoginToken {
 | 
				
			||||||
    #[serde(rename = "e")]
 | 
					    #[serde(rename = "e")]
 | 
				
			||||||
    expire: u64,
 | 
					    expire: u64,
 | 
				
			||||||
    #[serde(rename = "h")]
 | 
					    #[serde(rename = "h")]
 | 
				
			||||||
    hb: u64,
 | 
					    hb: u64,
 | 
				
			||||||
 | 
					    #[serde(rename = "t")]
 | 
				
			||||||
 | 
					    key: String,
 | 
				
			||||||
    #[serde(rename = "u")]
 | 
					    #[serde(rename = "u")]
 | 
				
			||||||
    pub user_id: UserID,
 | 
					    pub user_id: UserID,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl LoginToken {
 | 
					impl LoginToken {
 | 
				
			||||||
    pub fn new(user: &User) -> (String, Self) {
 | 
					    fn new(user_id: UserID) -> Self {
 | 
				
			||||||
        let key = format!("{}-{}", user.id().0, string_utils::rand_str(40));
 | 
					        Self {
 | 
				
			||||||
 | 
					            expire: time() + AUTH_TOKEN_DURATION.as_secs(),
 | 
				
			||||||
        (
 | 
					            hb: time(),
 | 
				
			||||||
            key,
 | 
					            key: rand_str(40),
 | 
				
			||||||
            Self {
 | 
					            user_id,
 | 
				
			||||||
                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<Self> {
 | 
					 | 
				
			||||||
        if self.hb + 60 * 5 < time() {
 | 
					 | 
				
			||||||
            let mut new = self.clone();
 | 
					 | 
				
			||||||
            new.hb = time();
 | 
					 | 
				
			||||||
            Some(new)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            None
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn redis_lifetime(&self) -> Duration {
 | 
					    fn is_expired(&self) -> bool {
 | 
				
			||||||
        Duration::from_secs(if self.expire <= time() {
 | 
					        self.expire < time() || self.hb + AUTH_TOKEN_MAX_INACTIVITY.as_secs() < time()
 | 
				
			||||||
            0
 | 
					    }
 | 
				
			||||||
        } else {
 | 
					
 | 
				
			||||||
            self.expire - 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
 | 
					/// Get redis key for a given user
 | 
				
			||||||
fn redis_key(tok: &str) -> String {
 | 
					fn redis_key(user_id: &UserID) -> String {
 | 
				
			||||||
    format!("tok-{tok}")
 | 
					    format!("tok-{}", user_id.0)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Get the tokens of a user
 | 
				
			||||||
 | 
					async fn get_user_tokens(user_id: UserID) -> anyhow::Result<Vec<LoginToken>> {
 | 
				
			||||||
 | 
					    Ok(redis_connection::get_value(&redis_key(&user_id))
 | 
				
			||||||
 | 
					        .await?
 | 
				
			||||||
 | 
					        .unwrap_or_default())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn set_user_tokens(user_id: UserID, mut tokens: Vec<LoginToken>) -> 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
 | 
					/// Generate a new login token
 | 
				
			||||||
pub async fn gen_new_token(user: &User) -> anyhow::Result<String> {
 | 
					pub async fn gen_new_token(user: &User) -> anyhow::Result<String> {
 | 
				
			||||||
    let (key, token) = LoginToken::new(user);
 | 
					    let mut tokens = get_user_tokens(user.id()).await?;
 | 
				
			||||||
    redis_connection::set_value(&redis_key(&key), &token, token.redis_lifetime()).await?;
 | 
					
 | 
				
			||||||
 | 
					    let token = LoginToken::new(user.id());
 | 
				
			||||||
 | 
					    let key = token.key();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tokens.push(token);
 | 
				
			||||||
 | 
					    set_user_tokens(user.id(), tokens).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(key)
 | 
					    Ok(key)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Delete a login token
 | 
					/// Delete a login token (disconnect a client)
 | 
				
			||||||
pub async fn delete_token(token: &LoginTokenValue) -> anyhow::Result<()> {
 | 
					pub async fn delete_token(token: &LoginTokenValue) -> anyhow::Result<()> {
 | 
				
			||||||
    redis_connection::remove_value(&redis_key(&token.0)).await?;
 | 
					    let (user_id, key) = token.parse()?;
 | 
				
			||||||
    Ok(())
 | 
					    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
 | 
					/// Get a user information from its token
 | 
				
			||||||
async fn get_token(key: &str) -> anyhow::Result<Option<LoginToken>> {
 | 
					async fn load_token_info(token: &LoginTokenValue) -> anyhow::Result<Option<LoginToken>> {
 | 
				
			||||||
    let token = match redis_connection::get_value::<LoginToken>(&redis_key(key)).await? {
 | 
					    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 => {
 | 
					        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);
 | 
					            return Ok(None);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        Some(t) => t,
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if token.is_expired() {
 | 
					    if token.is_expired() {
 | 
				
			||||||
@@ -93,11 +136,12 @@ async fn get_token(key: &str) -> anyhow::Result<Option<LoginToken>> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Check if heartbeat must be updated
 | 
					    // Check if heartbeat must be updated
 | 
				
			||||||
    if let Some(renew) = token.refresh_hb() {
 | 
					    let clone = token.clone();
 | 
				
			||||||
        redis_connection::set_value(&redis_key(key), &renew, renew.redis_lifetime()).await?;
 | 
					    if token.refresh_hb() {
 | 
				
			||||||
 | 
					        set_user_tokens(clone.user_id, user_tokens).await?;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(Some(token))
 | 
					    Ok(Some(clone))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl FromRequest for LoginTokenValue {
 | 
					impl FromRequest for LoginTokenValue {
 | 
				
			||||||
@@ -123,7 +167,7 @@ impl FromRequest for LoginToken {
 | 
				
			|||||||
        Box::pin(async move {
 | 
					        Box::pin(async move {
 | 
				
			||||||
            let token = LoginTokenValue::extract(&req).await?;
 | 
					            let token = LoginTokenValue::extract(&req).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let token = match get_token(&token.0).await {
 | 
					            let token = match load_token_info(&token).await {
 | 
				
			||||||
                Err(e) => {
 | 
					                Err(e) => {
 | 
				
			||||||
                    log::error!("Failed to load auth token! {}", e);
 | 
					                    log::error!("Failed to load auth token! {}", e);
 | 
				
			||||||
                    return Err(actix_web::error::ErrorInternalServerError(
 | 
					                    return Err(actix_web::error::ErrorInternalServerError(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ use crate::connections::db_connection;
 | 
				
			|||||||
use crate::constants::{ACCOUNT_DELETE_TOKEN_DURATION, PASSWORD_RESET_TOKEN_DURATION};
 | 
					use crate::constants::{ACCOUNT_DELETE_TOKEN_DURATION, PASSWORD_RESET_TOKEN_DURATION};
 | 
				
			||||||
use crate::models::{NewUser, User, UserID};
 | 
					use crate::models::{NewUser, User, UserID};
 | 
				
			||||||
use crate::schema::users;
 | 
					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::string_utils::rand_str;
 | 
				
			||||||
use crate::utils::time_utils::time;
 | 
					use crate::utils::time_utils::time;
 | 
				
			||||||
use bcrypt::DEFAULT_COST;
 | 
					use bcrypt::DEFAULT_COST;
 | 
				
			||||||
@@ -173,6 +173,8 @@ pub async fn delete_account(user: &User) -> anyhow::Result<()> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // TODO : remove families memberships
 | 
					    // TODO : remove families memberships
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    login_token_service::disconnect_user_from_all_devices(user.id()).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db_connection::execute(|conn| {
 | 
					    db_connection::execute(|conn| {
 | 
				
			||||||
        diesel::delete(users::dsl::users.filter(users::dsl::id.eq(user.id))).execute(conn)?;
 | 
					        diesel::delete(users::dsl::users.filter(users::dsl::id.eq(user.id))).execute(conn)?;
 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user