Files
GeneIT/geneit_backend/src/services/login_token_service.rs
Pierre HUBERT 776d24031b
All checks were successful
continuous-integration/drone/push Build is passing
Fix cargo clippy issues
2025-07-03 08:28:00 +02:00

189 lines
5.3 KiB
Rust

//! # 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::rand_str;
use crate::utils::time_utils::time;
use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest};
use std::future::{Ready, ready};
#[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 {
fn new(user_id: UserID) -> Self {
Self {
expire: time() + AUTH_TOKEN_DURATION.as_secs(),
hb: time(),
key: rand_str(40),
user_id,
}
}
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 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<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
pub async fn gen_new_token(user: &User) -> anyhow::Result<String> {
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 (disconnect a client)
pub async fn delete_token(token: &LoginTokenValue) -> anyhow::Result<()> {
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 load_token_info(token: &LoginTokenValue) -> anyhow::Result<Option<LoginToken>> {
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}' (missing token)");
return Ok(None);
}
};
if token.is_expired() {
log::error!("Could not find token for key '{key}' (token expired)");
return Ok(None);
}
// Check if heartbeat must be updated
let clone = token.clone();
if token.refresh_hb() {
set_user_tokens(clone.user_id, user_tokens).await?;
}
Ok(Some(clone))
}
impl FromRequest for LoginTokenValue {
type Error = actix_web::Error;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
ready(match req.headers().get("X-Auth-Token") {
Some(v) => Ok(LoginTokenValue(v.to_str().unwrap_or("").to_string())),
None => Err(actix_web::error::ErrorBadRequest(
"Missing auth token header!",
)),
})
}
}
impl FromRequest for LoginToken {
type Error = actix_web::Error;
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let req = req.clone();
Box::pin(async move {
let token = LoginTokenValue::extract(&req).await?;
let token = match load_token_info(&token).await {
Err(e) => {
log::error!("Failed to load auth token! {e}");
return Err(actix_web::error::ErrorPreconditionFailed(
"Failed to check auth token!",
));
}
Ok(None) => {
return Err(actix_web::error::ErrorPreconditionFailed(
"Invalid auth token!",
));
}
Ok(Some(t)) => t,
};
Ok(token)
})
}
}