Files
papercrate/backend/src/auth/webdav_tokens.rs
T
2025-10-31 20:37:07 +01:00

241 lines
6.6 KiB
Rust

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<String>,
expires_at: Option<NaiveDateTime>,
) -> Result<IssuedWebdavToken, AppError> {
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::<WebdavToken>(conn)?;
Ok(IssuedWebdavToken {
token: raw_secret,
record,
})
}
pub fn list_webdav_tokens(
conn: &mut PgPooledConnection,
user_id: Uuid,
tenant_id: Option<Uuid>,
) -> Result<Vec<WebdavToken>, 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::<WebdavToken>(conn)?;
Ok(tokens)
}
pub fn regenerate_webdav_token(
conn: &mut PgPooledConnection,
token_id: Uuid,
user_id: Uuid,
tenant_id: Option<Uuid>,
) -> Result<IssuedWebdavToken, AppError> {
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::<WebdavToken>(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::<Option<NaiveDateTime>>(None),
))
.get_result::<WebdavToken>(conn)?;
Ok(IssuedWebdavToken {
token: raw_secret,
record: updated,
})
}
pub fn find_active_token_by_secret(
conn: &mut PgPooledConnection,
user_id: Uuid,
tenant_id: Option<Uuid>,
secret: &str,
) -> Result<Option<WebdavToken>, 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::<WebdavToken>(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<bool, AppError> {
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<String, AppError> {
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<String, AppError> {
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());
}
}