Refactor login tokens management
This commit is contained in:
parent
f54dfde7f7
commit
9dd3811136
@ -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(())
|
||||||
|
Loading…
Reference in New Issue
Block a user