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 { 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(()) }