oxify_authn/
types.rs

1//! Authentication types and data structures
2//!
3//! Ported from `OxiRS` (<https://github.com/cool-japan/oxirs>)
4//! Original implementation: Copyright (c) `OxiRS` Contributors
5//! Adapted for `OxiFY`
6//! License: MIT OR Apache-2.0 (compatible with `OxiRS`)
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11/// Authentication result
12#[derive(Debug, Clone)]
13pub enum AuthResult {
14    Authenticated(User),
15    Unauthenticated,
16    Forbidden,
17    Expired,
18    Invalid,
19    Locked,
20    MfaRequired,
21}
22
23/// Authenticated user information
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct User {
26    pub username: String,
27    pub roles: Vec<String>,
28    pub email: Option<String>,
29    pub full_name: Option<String>,
30    pub last_login: Option<DateTime<Utc>>,
31    pub permissions: Vec<Permission>,
32}
33
34/// Permission types
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
36#[serde(rename_all = "snake_case")]
37pub enum Permission {
38    Read,
39    Write,
40    Admin,
41    GlobalAdmin,
42    GlobalRead,
43    GlobalWrite,
44    DatasetCreate,
45    DatasetDelete,
46    DatasetManage,
47    DatasetRead(String),
48    DatasetWrite(String),
49    DatasetAdmin(String),
50    UserManage,
51    SystemConfig,
52    SystemMetrics,
53    QueryExecute,
54    UpdateExecute,
55    Monitor,
56    Audit,
57}
58
59/// JWT token claims
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct Claims {
62    /// Subject (user identifier)
63    pub sub: String,
64    /// User roles
65    pub roles: Vec<String>,
66    /// User permissions
67    pub permissions: Vec<Permission>,
68    /// Expiration time (Unix timestamp)
69    pub exp: i64,
70    /// Issued at (Unix timestamp)
71    pub iat: i64,
72    /// Not before (Unix timestamp)
73    pub nbf: i64,
74    /// Issuer
75    pub iss: String,
76    /// Audience
77    pub aud: String,
78    /// JWT ID (unique identifier for revocation)
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub jti: Option<String>,
81    /// User email (optional)
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub email: Option<String>,
84    /// Token type (access, refresh, etc.)
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub token_type: Option<String>,
87}
88
89/// Login request
90#[derive(Debug, Deserialize)]
91pub struct LoginRequest {
92    pub username: String,
93    pub password: String,
94    pub mfa_token: Option<String>,
95}
96
97/// Login response
98#[derive(Debug, Serialize)]
99pub struct LoginResponse {
100    pub token: String,
101    pub user: User,
102    pub mfa_required: bool,
103    pub expires_at: Option<DateTime<Utc>>,
104    pub message: String,
105}
106
107/// Token validation result
108#[derive(Debug)]
109pub struct TokenValidation {
110    pub user: User,
111    pub expires_at: DateTime<Utc>,
112}
113
114/// Authentication errors
115#[derive(Debug, thiserror::Error)]
116pub enum AuthError {
117    #[error("Invalid credentials")]
118    InvalidCredentials,
119
120    #[error("Token expired")]
121    TokenExpired,
122
123    #[error("Invalid token: {0}")]
124    InvalidToken(String),
125
126    #[error("Token revoked")]
127    TokenRevoked,
128
129    #[error("Invalid input: {0}")]
130    InvalidInput(String),
131
132    #[error("MFA required")]
133    MfaRequired,
134
135    #[error("Invalid MFA token")]
136    InvalidMfaToken,
137
138    #[error("User not found")]
139    UserNotFound,
140
141    #[error("User disabled")]
142    UserDisabled,
143
144    #[error("User locked")]
145    UserLocked,
146
147    #[error("Permission denied")]
148    PermissionDenied,
149
150    #[error("SAML error: {0}")]
151    SamlError(String),
152
153    #[error("LDAP error: {0}")]
154    LdapError(String),
155
156    #[error("OAuth error: {0}")]
157    OAuthError(String),
158
159    #[error("Configuration error: {0}")]
160    ConfigurationError(String),
161
162    #[error("Internal error: {0}")]
163    InternalError(String),
164}
165
166pub type Result<T> = std::result::Result<T, AuthError>;
167
168/// JWT algorithm types
169#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
170pub enum JwtAlgorithm {
171    /// HMAC with SHA-256 (symmetric)
172    #[default]
173    HS256,
174    /// HMAC with SHA-384 (symmetric)
175    HS384,
176    /// HMAC with SHA-512 (symmetric)
177    HS512,
178    /// RSA with SHA-256 (asymmetric)
179    RS256,
180    /// RSA with SHA-384 (asymmetric)
181    RS384,
182    /// RSA with SHA-512 (asymmetric)
183    RS512,
184    /// ECDSA with SHA-256 (asymmetric)
185    ES256,
186    /// ECDSA with SHA-384 (asymmetric)
187    ES384,
188}
189
190/// JWT key configuration
191#[derive(Debug, Clone)]
192pub enum JwtKeyConfig {
193    /// Symmetric secret (for HS256/384/512)
194    Secret(String),
195    /// RSA key pair (PEM format)
196    Rsa {
197        private_key: Option<String>,
198        public_key: String,
199    },
200    /// EC key pair (PEM format)
201    Ec {
202        private_key: Option<String>,
203        public_key: String,
204    },
205}
206
207/// JWT configuration
208#[derive(Debug, Clone)]
209pub struct JwtConfig {
210    /// Secret or key material
211    pub secret: String,
212    /// Token issuer
213    pub issuer: String,
214    /// Token audience
215    pub audience: String,
216    /// Access token expiration in seconds
217    pub expiration_secs: u64,
218    /// Refresh token expiration in seconds
219    pub refresh_expiration_secs: u64,
220    /// Algorithm to use
221    pub algorithm: JwtAlgorithm,
222    /// Key configuration (for asymmetric algorithms)
223    pub key_config: Option<JwtKeyConfig>,
224    /// Include JWT ID (jti) claim
225    pub include_jti: bool,
226}
227
228impl JwtConfig {
229    /// Create a new JWT configuration with symmetric secret
230    pub fn new(
231        secret: impl Into<String>,
232        issuer: impl Into<String>,
233        audience: impl Into<String>,
234        expiration_secs: u64,
235    ) -> Self {
236        Self {
237            secret: secret.into(),
238            issuer: issuer.into(),
239            audience: audience.into(),
240            expiration_secs,
241            refresh_expiration_secs: 86400 * 30, // 30 days
242            algorithm: JwtAlgorithm::HS256,
243            key_config: None,
244            include_jti: false,
245        }
246    }
247
248    /// Default configuration for development
249    #[must_use]
250    pub fn development() -> Self {
251        Self {
252            secret: "dev-secret-change-in-production".to_string(),
253            issuer: "oxify-dev".to_string(),
254            audience: "oxify-api".to_string(),
255            expiration_secs: 3600,               // 1 hour
256            refresh_expiration_secs: 86400 * 30, // 30 days
257            algorithm: JwtAlgorithm::HS256,
258            key_config: None,
259            include_jti: true,
260        }
261    }
262
263    /// Create configuration with RS256
264    pub fn with_rsa(
265        private_key: impl Into<String>,
266        public_key: impl Into<String>,
267        issuer: impl Into<String>,
268        audience: impl Into<String>,
269        expiration_secs: u64,
270    ) -> Self {
271        Self {
272            secret: String::new(),
273            issuer: issuer.into(),
274            audience: audience.into(),
275            expiration_secs,
276            refresh_expiration_secs: 86400 * 30,
277            algorithm: JwtAlgorithm::RS256,
278            key_config: Some(JwtKeyConfig::Rsa {
279                private_key: Some(private_key.into()),
280                public_key: public_key.into(),
281            }),
282            include_jti: true,
283        }
284    }
285
286    /// Create configuration for verification only (public key only)
287    pub fn verification_only_rsa(
288        public_key: impl Into<String>,
289        issuer: impl Into<String>,
290        audience: impl Into<String>,
291    ) -> Self {
292        Self {
293            secret: String::new(),
294            issuer: issuer.into(),
295            audience: audience.into(),
296            expiration_secs: 3600,
297            refresh_expiration_secs: 86400 * 30,
298            algorithm: JwtAlgorithm::RS256,
299            key_config: Some(JwtKeyConfig::Rsa {
300                private_key: None,
301                public_key: public_key.into(),
302            }),
303            include_jti: false,
304        }
305    }
306
307    /// Set algorithm
308    #[must_use]
309    pub fn with_algorithm(mut self, algorithm: JwtAlgorithm) -> Self {
310        self.algorithm = algorithm;
311        self
312    }
313
314    /// Enable JWT ID generation
315    #[must_use]
316    pub fn with_jti(mut self, include_jti: bool) -> Self {
317        self.include_jti = include_jti;
318        self
319    }
320
321    /// Set refresh token expiration
322    #[must_use]
323    pub fn with_refresh_expiration(mut self, secs: u64) -> Self {
324        self.refresh_expiration_secs = secs;
325        self
326    }
327}
328
329/// `OAuth2` configuration
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct OAuth2Config {
332    pub provider: String,
333    pub client_id: String,
334    pub client_secret: String,
335    pub auth_url: String,
336    pub token_url: String,
337    pub user_info_url: String,
338    pub scopes: Vec<String>,
339}
340
341impl OAuth2Config {
342    /// Create a new `OAuth2` configuration
343    pub fn new(
344        provider: impl Into<String>,
345        client_id: impl Into<String>,
346        client_secret: impl Into<String>,
347        auth_url: impl Into<String>,
348        token_url: impl Into<String>,
349        user_info_url: impl Into<String>,
350    ) -> Self {
351        Self {
352            provider: provider.into(),
353            client_id: client_id.into(),
354            client_secret: client_secret.into(),
355            auth_url: auth_url.into(),
356            token_url: token_url.into(),
357            user_info_url: user_info_url.into(),
358            scopes: vec![
359                "openid".to_string(),
360                "profile".to_string(),
361                "email".to_string(),
362            ],
363        }
364    }
365
366    /// Create GitHub OAuth configuration
367    pub fn github(client_id: impl Into<String>, client_secret: impl Into<String>) -> Self {
368        Self::new(
369            "github",
370            client_id,
371            client_secret,
372            "https://github.com/login/oauth/authorize",
373            "https://github.com/login/oauth/access_token",
374            "https://api.github.com/user",
375        )
376    }
377
378    /// Create Google OAuth configuration
379    pub fn google(client_id: impl Into<String>, client_secret: impl Into<String>) -> Self {
380        Self::new(
381            "google",
382            client_id,
383            client_secret,
384            "https://accounts.google.com/o/oauth2/v2/auth",
385            "https://oauth2.googleapis.com/token",
386            "https://www.googleapis.com/oauth2/v2/userinfo",
387        )
388    }
389}
390
391// ============================================================================
392// Audit Events
393// ============================================================================
394
395/// Authentication audit event types for logging and compliance
396#[derive(Debug, Clone, Serialize, Deserialize)]
397pub enum AuditEventType {
398    // Login events
399    /// Successful login
400    LoginSuccess,
401    /// Failed login attempt
402    LoginFailure,
403    /// User logout
404    Logout,
405    /// Session expired
406    SessionExpired,
407
408    // MFA events
409    /// MFA enrollment started
410    MfaEnrollmentStarted,
411    /// MFA enrollment completed
412    MfaEnrollmentCompleted,
413    /// MFA challenge sent
414    MfaChallengeSent,
415    /// MFA verification succeeded
416    MfaVerificationSuccess,
417    /// MFA verification failed
418    MfaVerificationFailure,
419    /// Backup code used
420    MfaBackupCodeUsed,
421
422    // Password events
423    /// Password changed
424    PasswordChanged,
425    /// Password reset requested
426    PasswordResetRequested,
427    /// Password reset completed
428    PasswordResetCompleted,
429
430    // Token events
431    /// Access token issued
432    TokenIssued,
433    /// Token refreshed
434    TokenRefreshed,
435    /// Token revoked
436    TokenRevoked,
437    /// Token validation failed
438    TokenValidationFailed,
439
440    // Account events
441    /// Account created
442    AccountCreated,
443    /// Account updated
444    AccountUpdated,
445    /// Account disabled
446    AccountDisabled,
447    /// Account enabled
448    AccountEnabled,
449    /// Account deleted
450    AccountDeleted,
451    /// Account locked due to failed attempts
452    AccountLocked,
453    /// Account unlocked
454    AccountUnlocked,
455
456    // OAuth events
457    /// OAuth authorization started
458    OAuthAuthorizationStarted,
459    /// OAuth callback received
460    OAuthCallbackReceived,
461    /// OAuth token exchange completed
462    OAuthTokenExchanged,
463
464    // Security events
465    /// Suspicious activity detected
466    SuspiciousActivityDetected,
467    /// Rate limit exceeded
468    RateLimitExceeded,
469    /// Permission denied
470    PermissionDenied,
471    /// IP blocked
472    IpBlocked,
473
474    // Session events
475    /// Session created
476    SessionCreated,
477    /// Session invalidated
478    SessionInvalidated,
479    /// All sessions invalidated (force logout)
480    AllSessionsInvalidated,
481
482    // Admin events
483    /// Admin action performed
484    AdminAction(String),
485}
486
487/// Audit event with context
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct AuditEvent {
490    /// Event type
491    pub event_type: AuditEventType,
492    /// Event timestamp
493    pub timestamp: DateTime<Utc>,
494    /// User ID (if applicable)
495    pub user_id: Option<String>,
496    /// IP address
497    pub ip_address: Option<String>,
498    /// User agent
499    pub user_agent: Option<String>,
500    /// Session ID (if applicable)
501    pub session_id: Option<String>,
502    /// Request ID for tracing
503    pub request_id: Option<String>,
504    /// Success indicator
505    pub success: bool,
506    /// Failure reason (if failed)
507    pub failure_reason: Option<String>,
508    /// Additional metadata
509    pub metadata: std::collections::HashMap<String, String>,
510}
511
512impl AuditEvent {
513    /// Create a new audit event
514    #[must_use]
515    pub fn new(event_type: AuditEventType) -> Self {
516        Self {
517            event_type,
518            timestamp: Utc::now(),
519            user_id: None,
520            ip_address: None,
521            user_agent: None,
522            session_id: None,
523            request_id: None,
524            success: true,
525            failure_reason: None,
526            metadata: std::collections::HashMap::new(),
527        }
528    }
529
530    /// Set user ID
531    #[must_use]
532    pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
533        self.user_id = Some(user_id.into());
534        self
535    }
536
537    /// Set IP address
538    #[must_use]
539    pub fn with_ip(mut self, ip: impl Into<String>) -> Self {
540        self.ip_address = Some(ip.into());
541        self
542    }
543
544    /// Set user agent
545    #[must_use]
546    pub fn with_user_agent(mut self, ua: impl Into<String>) -> Self {
547        self.user_agent = Some(ua.into());
548        self
549    }
550
551    /// Set session ID
552    #[must_use]
553    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
554        self.session_id = Some(session_id.into());
555        self
556    }
557
558    /// Set request ID
559    #[must_use]
560    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
561        self.request_id = Some(request_id.into());
562        self
563    }
564
565    /// Mark as failed
566    #[must_use]
567    pub fn failed(mut self, reason: impl Into<String>) -> Self {
568        self.success = false;
569        self.failure_reason = Some(reason.into());
570        self
571    }
572
573    /// Add metadata
574    #[must_use]
575    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
576        self.metadata.insert(key.into(), value.into());
577        self
578    }
579
580    /// Create a login success event
581    pub fn login_success(user_id: impl Into<String>) -> Self {
582        Self::new(AuditEventType::LoginSuccess).with_user(user_id)
583    }
584
585    /// Create a login failure event
586    pub fn login_failure(username: impl Into<String>, reason: impl Into<String>) -> Self {
587        Self::new(AuditEventType::LoginFailure)
588            .with_metadata("username", username)
589            .failed(reason)
590    }
591
592    /// Create a logout event
593    pub fn logout(user_id: impl Into<String>) -> Self {
594        Self::new(AuditEventType::Logout).with_user(user_id)
595    }
596
597    /// Create a token issued event
598    pub fn token_issued(user_id: impl Into<String>, token_type: impl Into<String>) -> Self {
599        Self::new(AuditEventType::TokenIssued)
600            .with_user(user_id)
601            .with_metadata("token_type", token_type)
602    }
603
604    /// Create a password changed event
605    pub fn password_changed(user_id: impl Into<String>) -> Self {
606        Self::new(AuditEventType::PasswordChanged).with_user(user_id)
607    }
608
609    /// Create an MFA verification event
610    pub fn mfa_verification(user_id: impl Into<String>, success: bool) -> Self {
611        let event_type = if success {
612            AuditEventType::MfaVerificationSuccess
613        } else {
614            AuditEventType::MfaVerificationFailure
615        };
616        let event = Self::new(event_type).with_user(user_id);
617        if success {
618            event
619        } else {
620            event.failed("MFA verification failed")
621        }
622    }
623
624    /// Create a rate limit exceeded event
625    pub fn rate_limit_exceeded(ip: impl Into<String>) -> Self {
626        Self::new(AuditEventType::RateLimitExceeded)
627            .with_ip(ip)
628            .failed("Rate limit exceeded")
629    }
630}
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635
636    #[test]
637    fn test_user_creation() {
638        let user = User {
639            username: "alice".to_string(),
640            roles: vec!["admin".to_string()],
641            email: Some("alice@example.com".to_string()),
642            full_name: Some("Alice Wonderland".to_string()),
643            last_login: None,
644            permissions: vec![Permission::Admin, Permission::Read],
645        };
646
647        assert_eq!(user.username, "alice");
648        assert_eq!(user.roles.len(), 1);
649        assert_eq!(user.permissions.len(), 2);
650    }
651
652    #[test]
653    fn test_jwt_config_development() {
654        let config = JwtConfig::development();
655        assert_eq!(config.issuer, "oxify-dev");
656        assert_eq!(config.expiration_secs, 3600);
657    }
658
659    #[test]
660    fn test_permission_ordering() {
661        let mut perms = vec![Permission::Write, Permission::Admin, Permission::Read];
662        perms.sort();
663        // Permissions sort by variant order, not alphabetically
664        assert_eq!(
665            perms,
666            vec![Permission::Read, Permission::Write, Permission::Admin]
667        );
668    }
669
670    #[test]
671    fn test_audit_event_login_success() {
672        let event = AuditEvent::login_success("user123")
673            .with_ip("192.168.1.1")
674            .with_user_agent("Mozilla/5.0");
675
676        assert!(matches!(event.event_type, AuditEventType::LoginSuccess));
677        assert_eq!(event.user_id, Some("user123".to_string()));
678        assert_eq!(event.ip_address, Some("192.168.1.1".to_string()));
679        assert!(event.success);
680    }
681
682    #[test]
683    fn test_audit_event_login_failure() {
684        let event = AuditEvent::login_failure("baduser", "Invalid credentials");
685
686        assert!(matches!(event.event_type, AuditEventType::LoginFailure));
687        assert!(!event.success);
688        assert_eq!(
689            event.failure_reason,
690            Some("Invalid credentials".to_string())
691        );
692        assert_eq!(event.metadata.get("username"), Some(&"baduser".to_string()));
693    }
694
695    #[test]
696    fn test_audit_event_builder() {
697        let event = AuditEvent::new(AuditEventType::TokenIssued)
698            .with_user("user123")
699            .with_ip("10.0.0.1")
700            .with_session("session-abc")
701            .with_request_id("req-123")
702            .with_metadata("token_type", "access");
703
704        assert_eq!(event.user_id, Some("user123".to_string()));
705        assert_eq!(event.ip_address, Some("10.0.0.1".to_string()));
706        assert_eq!(event.session_id, Some("session-abc".to_string()));
707        assert_eq!(event.request_id, Some("req-123".to_string()));
708        assert_eq!(
709            event.metadata.get("token_type"),
710            Some(&"access".to_string())
711        );
712    }
713}