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>
181 lines
4.7 KiB
Rust
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\"");
|
|
}
|
|
}
|