Skip to content
Clone
main
87 lines 3.3 KB Rust
use crate::models::users::{user_mfa, user_mfa_backup_codes};
use sea_orm::sea_query::{Expr, ExprTrait};
use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter};
use crate::{AuthError, AuthResult};
#[tracing::instrument(skip_all, fields(request_id = %envelope::context::resolve_request_id(_request_id)))]
pub async fn consume_totp(
db: &crate::db::DbConn,
factor: &user_mfa::Model,
key: Option<&str>,
code: &str,
_request_id: Option<&str>,
) -> AuthResult<u64> {
let ciphertext = factor
.secret_ciphertext
.as_deref()
.ok_or(AuthError::MfaRequired)?;
let secret = super::totp::decrypt_secret(key, ciphertext)?;
let (step, value) = super::totp::verify_totp_code(&secret, code, factor.last_totp_step)?;
let now = chrono::Utc::now();
let updated = user_mfa::Entity::update_many()
.col_expr(user_mfa::Column::LastTotpStep, Expr::value(value))
.col_expr(user_mfa::Column::MfaLastUsedAt, Expr::value(now))
.col_expr(
user_mfa::Column::Version,
Expr::col(user_mfa::Column::Version).add(1),
)
.col_expr(user_mfa::Column::UpdatedAt, Expr::value(now))
.filter(user_mfa::Column::Id.eq(factor.id))
.filter(user_mfa::Column::Version.eq(factor.version))
.filter(user_mfa::Column::MfaStatus.eq(user_mfa::MfaStatus::Enabled))
.filter(user_mfa::Column::RevokedAt.is_null())
.filter(
Condition::any()
.add(user_mfa::Column::LastTotpStep.is_null())
.add(user_mfa::Column::LastTotpStep.lt(value)),
)
.exec(db.write())
.await?;
if updated.rows_affected != 1 {
return Err(AuthError::TotpStepReused);
}
Ok(step)
}
#[tracing::instrument(skip_all, fields(user_id = user_id, request_id = %envelope::context::resolve_request_id(_request_id)))]
pub async fn consume_recovery(
db: &crate::db::DbConn,
user_id: i64,
code: &str,
_request_id: Option<&str>,
) -> AuthResult<()> {
let normalized: String = code
.trim()
.to_uppercase()
.chars()
.filter(|value| *value != '-')
.collect();
if normalized.is_empty() {
return Err(AuthError::RecoveryCodeInvalid);
}
let hash = super::crypto::sha256_hex(normalized.as_bytes());
let now = chrono::Utc::now();
let updated = user_mfa_backup_codes::Entity::update_many()
.col_expr(user_mfa_backup_codes::Column::ConsumedAt, Expr::value(now))
.col_expr(
user_mfa_backup_codes::Column::Version,
Expr::col(user_mfa_backup_codes::Column::Version).add(1),
)
.filter(user_mfa_backup_codes::Column::UserId.eq(user_id))
.filter(user_mfa_backup_codes::Column::CodeHash.eq(hash))
.filter(user_mfa_backup_codes::Column::HashKeyId.eq("sha256-v1"))
.filter(user_mfa_backup_codes::Column::ConsumedAt.is_null())
.filter(user_mfa_backup_codes::Column::RevokedAt.is_null())
.filter(
Condition::any()
.add(user_mfa_backup_codes::Column::ExpiresAt.is_null())
.add(user_mfa_backup_codes::Column::ExpiresAt.gt(now)),
)
.exec(db.write())
.await?;
if updated.rows_affected != 1 {
return Err(AuthError::RecoveryCodeInvalid);
}
Ok(())
}