Skip to main content

backbone_auth/
audit.rs

1//! Structured audit event types for authentication security logging
2//!
3//! These events provide a standardized format for security audit logging
4//! across the authentication system. They are emitted via the `tracing`
5//! crate and can be captured by any tracing subscriber (stdout, file,
6//! OpenTelemetry, etc.).
7//!
8//! # Security
9//!
10//! Audit events NEVER contain sensitive data such as passwords, tokens,
11//! password hashes, or JWT secrets. Only identifiers (user_id, email),
12//! metadata (ip_address, device_type), and outcomes are logged.
13
14use serde::Serialize;
15
16/// Structured security audit events emitted by the auth system
17#[derive(Debug, Clone, Serialize)]
18#[serde(tag = "event_type", rename_all = "snake_case")]
19pub enum AuditEvent {
20    /// Authentication attempt started
21    AuthAttemptStarted {
22        email: String,
23        ip_address: Option<String>,
24    },
25
26    /// Authentication succeeded
27    AuthSuccess {
28        user_id: String,
29        ip_address: Option<String>,
30        risk_score: Option<f32>,
31    },
32
33    /// Authentication failed
34    AuthFailure {
35        email: String,
36        reason: String,
37        ip_address: Option<String>,
38    },
39
40    /// Token generated
41    TokenGenerated {
42        user_id: String,
43        token_type: String,
44    },
45
46    /// Token validated successfully
47    TokenValidated {
48        user_id: String,
49    },
50
51    /// Token validation failed
52    TokenValidationFailed {
53        reason: String,
54    },
55
56    /// Password hashed (no sensitive data included)
57    PasswordHashed,
58
59    /// Password verification completed
60    PasswordVerified {
61        success: bool,
62    },
63
64    /// Password validation failed requirements
65    PasswordValidationFailed {
66        reason: String,
67    },
68
69    /// Account status checked during auth flow
70    AccountStatusChecked {
71        user_id: String,
72        is_active: bool,
73        is_locked: bool,
74    },
75
76    /// Two-factor authentication required
77    TwoFactorRequired {
78        user_id: String,
79    },
80
81    /// New device detected during login
82    NewDeviceDetected {
83        user_id: String,
84    },
85}
86
87impl AuditEvent {
88    /// Get a human-readable description of the event
89    pub fn description(&self) -> &'static str {
90        match self {
91            Self::AuthAttemptStarted { .. } => "Authentication attempt started",
92            Self::AuthSuccess { .. } => "Authentication successful",
93            Self::AuthFailure { .. } => "Authentication failed",
94            Self::TokenGenerated { .. } => "Token generated",
95            Self::TokenValidated { .. } => "Token validated",
96            Self::TokenValidationFailed { .. } => "Token validation failed",
97            Self::PasswordHashed => "Password hashed",
98            Self::PasswordVerified { .. } => "Password verified",
99            Self::PasswordValidationFailed { .. } => "Password validation failed",
100            Self::AccountStatusChecked { .. } => "Account status checked",
101            Self::TwoFactorRequired { .. } => "Two-factor authentication required",
102            Self::NewDeviceDetected { .. } => "New device detected",
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_audit_event_serialization() {
113        let event = AuditEvent::AuthSuccess {
114            user_id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
115            ip_address: Some("192.168.1.1".to_string()),
116            risk_score: Some(0.1),
117        };
118        let json = serde_json::to_string(&event).unwrap();
119        assert!(json.contains("auth_success"));
120        assert!(json.contains("192.168.1.1"));
121        assert!(json.contains("0.1"));
122    }
123
124    #[test]
125    fn test_audit_event_failure_serialization() {
126        let event = AuditEvent::AuthFailure {
127            email: "user@example.com".to_string(),
128            reason: "Invalid credentials".to_string(),
129            ip_address: None,
130        };
131        let json = serde_json::to_string(&event).unwrap();
132        assert!(json.contains("auth_failure"));
133        assert!(json.contains("Invalid credentials"));
134        // Verify no sensitive data could sneak in
135        assert!(!json.contains("password"));
136        assert!(!json.contains("token"));
137        assert!(!json.contains("hash"));
138    }
139
140    #[test]
141    fn test_audit_event_no_sensitive_fields() {
142        // The AuditEvent enum by design has no fields for passwords or tokens.
143        // This test documents that invariant.
144        let event = AuditEvent::PasswordVerified { success: false };
145        let json = serde_json::to_string(&event).unwrap();
146        assert!(!json.contains("password_value"));
147        assert!(!json.contains("secret"));
148    }
149
150    #[test]
151    fn test_audit_event_description() {
152        assert_eq!(
153            AuditEvent::AuthSuccess {
154                user_id: "123".to_string(),
155                ip_address: None,
156                risk_score: None,
157            }.description(),
158            "Authentication successful"
159        );
160        assert_eq!(
161            AuditEvent::AuthFailure {
162                email: "test@test.com".to_string(),
163                reason: "locked".to_string(),
164                ip_address: None,
165            }.description(),
166            "Authentication failed"
167        );
168        assert_eq!(
169            AuditEvent::TwoFactorRequired {
170                user_id: "123".to_string(),
171            }.description(),
172            "Two-factor authentication required"
173        );
174    }
175}