Files
foxhunt/services/api/src/auth/mfa/verification.rs
jgrusewski e50ea55064 feat: create services/api/ — unified gRPC gateway with tonic-web
Copied from api_gateway, removed REST handlers (port 8080),
added tonic-web + CORS for grpc-web browser access.
Binary renamed: api-gateway → api

Changes:
- Package name: api-gateway → api
- Deleted src/handlers/ (REST ML endpoints on port 8080)
- Added tonic-web 0.13 + tower-http CORS layer
- Server::builder().accept_http1(true) for grpc-web
- CORS_ORIGINS env var (default http://localhost:5173)
- Metrics server on port 9091 (axum) preserved
- All 95 lib tests pass, 0 clippy warnings
- Added services/api to workspace members

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:32:46 +01:00

181 lines
4.7 KiB
Rust

//! MFA Verification Flow
//!
//! Handles verification of MFA codes during authentication.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;
/// MFA verification request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MfaVerification {
/// User ID
pub user_id: Uuid,
/// Verification method
pub method: VerificationMethod,
/// Code to verify
pub code: String,
/// Client IP address (for audit)
pub ip_address: Option<String>,
/// User agent (for audit)
pub user_agent: Option<String>,
}
/// Verification method
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VerificationMethod {
/// TOTP code from authenticator app
Totp,
/// Backup recovery code
BackupCode,
/// Trusted device (future enhancement)
TrustedDevice,
}
/// Verification result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationResult {
/// Whether verification succeeded
pub success: bool,
/// User ID
pub user_id: Uuid,
/// Verification method used
pub method: VerificationMethod,
/// Timestamp of verification
pub timestamp: DateTime<Utc>,
/// Additional metadata
pub metadata: VerificationMetadata,
}
/// Verification metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationMetadata {
/// Number of failed attempts
pub failed_attempts: i32,
/// Whether account is locked
pub is_locked: bool,
/// Lock expiration (if locked)
pub locked_until: Option<DateTime<Utc>>,
/// Remaining backup codes (if applicable)
pub backup_codes_remaining: Option<i32>,
/// Time drift in TOTP verification (if applicable)
pub totp_drift: Option<i32>,
}
impl VerificationResult {
/// Create successful verification result
pub fn success(
user_id: Uuid,
method: VerificationMethod,
metadata: VerificationMetadata,
) -> Self {
Self {
success: true,
user_id,
method,
timestamp: Utc::now(),
metadata,
}
}
/// Create failed verification result
pub fn failure(
user_id: Uuid,
method: VerificationMethod,
metadata: VerificationMetadata,
) -> Self {
Self {
success: false,
user_id,
method,
timestamp: Utc::now(),
metadata,
}
}
}
/// Verification errors
#[derive(Debug, Error)]
pub enum VerificationError {
#[error("MFA not configured for user")]
NotConfigured,
#[error("MFA is not enabled for user")]
NotEnabled,
#[error("Account is locked due to too many failed attempts")]
AccountLocked,
#[error("Invalid verification code")]
InvalidCode,
#[error("Verification code has expired")]
CodeExpired,
#[error("Backup code already used")]
BackupCodeUsed,
#[error("No backup codes remaining")]
NoBackupCodesRemaining,
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Internal error: {0}")]
InternalError(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verification_result_success() {
let user_id = Uuid::new_v4();
let metadata = VerificationMetadata {
failed_attempts: 0,
is_locked: false,
locked_until: None,
backup_codes_remaining: Some(10),
totp_drift: None,
};
let result = VerificationResult::success(user_id, VerificationMethod::Totp, metadata);
assert!(result.success);
assert_eq!(result.user_id, user_id);
assert_eq!(result.method, VerificationMethod::Totp);
assert_eq!(result.metadata.failed_attempts, 0);
}
#[test]
fn test_verification_result_failure() {
let user_id = Uuid::new_v4();
let metadata = VerificationMetadata {
failed_attempts: 3,
is_locked: false,
locked_until: None,
backup_codes_remaining: Some(10),
totp_drift: None,
};
let result = VerificationResult::failure(user_id, VerificationMethod::Totp, metadata);
assert!(!result.success);
assert_eq!(result.metadata.failed_attempts, 3);
}
#[test]
fn test_verification_method_serialization() {
let method = VerificationMethod::Totp;
let json = serde_json::to_string(&method).expect("INVARIANT: Serialization should succeed for valid types");
assert_eq!(json, "\"totp\"");
let method = VerificationMethod::BackupCode;
let json = serde_json::to_string(&method).expect("INVARIANT: Serialization should succeed for valid types");
assert_eq!(json, "\"backup_code\"");
}
}