1use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone)]
13pub enum AuthResult {
14 Authenticated(User),
15 Unauthenticated,
16 Forbidden,
17 Expired,
18 Invalid,
19 Locked,
20 MfaRequired,
21}
22
23#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct Claims {
62 pub sub: String,
64 pub roles: Vec<String>,
66 pub permissions: Vec<Permission>,
68 pub exp: i64,
70 pub iat: i64,
72 pub nbf: i64,
74 pub iss: String,
76 pub aud: String,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub jti: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub email: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub token_type: Option<String>,
87}
88
89#[derive(Debug, Deserialize)]
91pub struct LoginRequest {
92 pub username: String,
93 pub password: String,
94 pub mfa_token: Option<String>,
95}
96
97#[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#[derive(Debug)]
109pub struct TokenValidation {
110 pub user: User,
111 pub expires_at: DateTime<Utc>,
112}
113
114#[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#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
170pub enum JwtAlgorithm {
171 #[default]
173 HS256,
174 HS384,
176 HS512,
178 RS256,
180 RS384,
182 RS512,
184 ES256,
186 ES384,
188}
189
190#[derive(Debug, Clone)]
192pub enum JwtKeyConfig {
193 Secret(String),
195 Rsa {
197 private_key: Option<String>,
198 public_key: String,
199 },
200 Ec {
202 private_key: Option<String>,
203 public_key: String,
204 },
205}
206
207#[derive(Debug, Clone)]
209pub struct JwtConfig {
210 pub secret: String,
212 pub issuer: String,
214 pub audience: String,
216 pub expiration_secs: u64,
218 pub refresh_expiration_secs: u64,
220 pub algorithm: JwtAlgorithm,
222 pub key_config: Option<JwtKeyConfig>,
224 pub include_jti: bool,
226}
227
228impl JwtConfig {
229 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, algorithm: JwtAlgorithm::HS256,
243 key_config: None,
244 include_jti: false,
245 }
246 }
247
248 #[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, refresh_expiration_secs: 86400 * 30, algorithm: JwtAlgorithm::HS256,
258 key_config: None,
259 include_jti: true,
260 }
261 }
262
263 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 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 #[must_use]
309 pub fn with_algorithm(mut self, algorithm: JwtAlgorithm) -> Self {
310 self.algorithm = algorithm;
311 self
312 }
313
314 #[must_use]
316 pub fn with_jti(mut self, include_jti: bool) -> Self {
317 self.include_jti = include_jti;
318 self
319 }
320
321 #[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#[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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
397pub enum AuditEventType {
398 LoginSuccess,
401 LoginFailure,
403 Logout,
405 SessionExpired,
407
408 MfaEnrollmentStarted,
411 MfaEnrollmentCompleted,
413 MfaChallengeSent,
415 MfaVerificationSuccess,
417 MfaVerificationFailure,
419 MfaBackupCodeUsed,
421
422 PasswordChanged,
425 PasswordResetRequested,
427 PasswordResetCompleted,
429
430 TokenIssued,
433 TokenRefreshed,
435 TokenRevoked,
437 TokenValidationFailed,
439
440 AccountCreated,
443 AccountUpdated,
445 AccountDisabled,
447 AccountEnabled,
449 AccountDeleted,
451 AccountLocked,
453 AccountUnlocked,
455
456 OAuthAuthorizationStarted,
459 OAuthCallbackReceived,
461 OAuthTokenExchanged,
463
464 SuspiciousActivityDetected,
467 RateLimitExceeded,
469 PermissionDenied,
471 IpBlocked,
473
474 SessionCreated,
477 SessionInvalidated,
479 AllSessionsInvalidated,
481
482 AdminAction(String),
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct AuditEvent {
490 pub event_type: AuditEventType,
492 pub timestamp: DateTime<Utc>,
494 pub user_id: Option<String>,
496 pub ip_address: Option<String>,
498 pub user_agent: Option<String>,
500 pub session_id: Option<String>,
502 pub request_id: Option<String>,
504 pub success: bool,
506 pub failure_reason: Option<String>,
508 pub metadata: std::collections::HashMap<String, String>,
510}
511
512impl AuditEvent {
513 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 pub fn login_success(user_id: impl Into<String>) -> Self {
582 Self::new(AuditEventType::LoginSuccess).with_user(user_id)
583 }
584
585 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 pub fn logout(user_id: impl Into<String>) -> Self {
594 Self::new(AuditEventType::Logout).with_user(user_id)
595 }
596
597 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 pub fn password_changed(user_id: impl Into<String>) -> Self {
606 Self::new(AuditEventType::PasswordChanged).with_user(user_id)
607 }
608
609 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 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 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}