use argon2::{ password_hash::{PasswordHasher, SaltString}, Argon2, }; use chrono::{NaiveDateTime, Utc}; use diesel::prelude::*; use rand::rngs::OsRng; use rand::RngCore; use uuid::Uuid; use crate::{ error::AppError, models::{NewWebdavToken, WebdavToken}, schema::webdav_tokens, state::PgPooledConnection, tenants::{apply_webdav_token_prefix, clear_webdav_token_prefix}, }; const TOKEN_PREFIX_LENGTH: usize = 12; const TOKEN_SECRET_LENGTH: usize = 32; pub struct IssuedWebdavToken { pub token: String, pub record: WebdavToken, } pub fn create_webdav_token( conn: &mut PgPooledConnection, user_id: Uuid, tenant_id: Uuid, label: Option, expires_at: Option, ) -> Result { let raw_secret = generate_secret()?; let token_prefix = raw_secret[..TOKEN_PREFIX_LENGTH].to_string(); let token_hash = hash_secret(&raw_secret)?; let new_token = NewWebdavToken { id: Uuid::new_v4(), user_id, tenant_id, token_prefix, token_hash, label, expires_at, }; let record = diesel::insert_into(webdav_tokens::table) .values(&new_token) .get_result::(conn)?; Ok(IssuedWebdavToken { token: raw_secret, record, }) } pub fn list_webdav_tokens( conn: &mut PgPooledConnection, user_id: Uuid, tenant_id: Option, ) -> Result, AppError> { let mut query = webdav_tokens::table .filter(webdav_tokens::user_id.eq(user_id)) .into_boxed(); if let Some(tenant_id) = tenant_id { query = query.filter(webdav_tokens::tenant_id.eq(tenant_id)); } let tokens = query .order(webdav_tokens::created_at.asc()) .load::(conn)?; Ok(tokens) } pub fn regenerate_webdav_token( conn: &mut PgPooledConnection, token_id: Uuid, user_id: Uuid, tenant_id: Option, ) -> Result { let mut query = webdav_tokens::table .filter(webdav_tokens::id.eq(token_id)) .filter(webdav_tokens::user_id.eq(user_id)) .into_boxed(); if let Some(tenant) = tenant_id { query = query.filter(webdav_tokens::tenant_id.eq(tenant)); } let record = query .first::(conn) .optional() .map_err(AppError::from)? .ok_or_else(AppError::not_found)?; if record.revoked_at.is_some() { return Err(AppError::bad_request( "cannot regenerate a revoked WebDAV token", )); } let raw_secret = generate_secret()?; let token_prefix = raw_secret[..TOKEN_PREFIX_LENGTH].to_string(); let token_hash = hash_secret(&raw_secret)?; let updated = diesel::update(webdav_tokens::table.find(record.id)) .set(( webdav_tokens::token_prefix.eq(&token_prefix), webdav_tokens::token_hash.eq(&token_hash), webdav_tokens::last_used_at.eq::>(None), )) .get_result::(conn)?; Ok(IssuedWebdavToken { token: raw_secret, record: updated, }) } pub fn find_active_token_by_secret( conn: &mut PgPooledConnection, user_id: Uuid, tenant_id: Option, secret: &str, ) -> Result, AppError> { if secret.len() < TOKEN_PREFIX_LENGTH { return Ok(None); } let prefix = &secret[..TOKEN_PREFIX_LENGTH]; apply_webdav_token_prefix(conn, prefix)?; let mut query = webdav_tokens::table .filter(webdav_tokens::user_id.eq(user_id)) .filter(webdav_tokens::token_prefix.eq(prefix)) .filter(webdav_tokens::revoked_at.is_null()) .into_boxed(); let now = Utc::now().naive_utc(); query = query.filter( webdav_tokens::expires_at .is_null() .or(webdav_tokens::expires_at.gt(now)), ); if let Some(tenant_id) = tenant_id { query = query.filter(webdav_tokens::tenant_id.eq(tenant_id)); } let load_result = query.load::(conn); let clear_result = clear_webdav_token_prefix(conn); clear_result?; let candidates = load_result?; for token in candidates { if verify_token_secret(secret, &token.token_hash)? { return Ok(Some(token)); } } Ok(None) } pub fn revoke_webdav_token( conn: &mut PgPooledConnection, token_id: Uuid, user_id: Uuid, ) -> Result<(), AppError> { let affected = diesel::update( webdav_tokens::table .filter(webdav_tokens::id.eq(token_id)) .filter(webdav_tokens::user_id.eq(user_id)), ) .set(webdav_tokens::revoked_at.eq(Utc::now().naive_utc())) .execute(conn)?; if affected == 0 { return Err(AppError::not_found()); } Ok(()) } pub fn touch_webdav_token(conn: &mut PgPooledConnection, token_id: Uuid) -> Result<(), AppError> { diesel::update(webdav_tokens::table.filter(webdav_tokens::id.eq(token_id))) .set(webdav_tokens::last_used_at.eq(Utc::now().naive_utc())) .execute(conn)?; Ok(()) } pub fn verify_token_secret(secret: &str, token_hash: &str) -> Result { crate::auth::password::verify_password(secret, token_hash).map_err(|err| { tracing::error!(error = ?err, "failed to verify token"); AppError::internal("failed to verify token") }) } fn generate_secret() -> Result { let mut buffer = [0u8; TOKEN_SECRET_LENGTH]; OsRng.try_fill_bytes(&mut buffer).map_err(|err| { tracing::error!(error = ?err, "failed to generate token"); AppError::internal("failed to generate token") })?; Ok(hex::encode(buffer)) } fn hash_secret(secret: &str) -> Result { let salt = SaltString::generate(&mut OsRng); let hash = Argon2::default() .hash_password(secret.as_bytes(), &salt) .map_err(|err| { tracing::error!(error = ?err, "failed to hash token"); AppError::internal("failed to hash token") })?; Ok(hash.to_string()) } fn _ensure_constants() { assert!(TOKEN_PREFIX_LENGTH < TOKEN_SECRET_LENGTH * 2); } #[cfg(test)] mod tests { use super::*; #[test] fn generated_secret_has_expected_length() { let secret = generate_secret().unwrap(); assert_eq!(secret.len(), TOKEN_SECRET_LENGTH * 2); } #[test] fn hash_and_verify_secret_round_trip() { let secret = generate_secret().unwrap(); let hash = hash_secret(&secret).unwrap(); assert!(verify_token_secret(&secret, &hash).unwrap()); assert!(!verify_token_secret("wrong", &hash).unwrap()); } }